Repository: 20c/munge

Issue:
add round trip support
For formats that libraries exist.

Discussion:
lgtm-com[bot]: This pull request **introduces 3 alerts** when merging d7aa2690b71bd7b42f81e7005434d6e5925af783 into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-88a56d100d93b266882122d87962bf72e675324f)

**new alerts:**

* 3 for Unused import
codecov-commenter: # [Codecov](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c) Report
> Merging [#15](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c) (716b825) into [master](https://codecov.io/gh/20c/munge/commit/882070dbec6a3600217e406c9501091d8c27feab?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c) (882070d) will **decrease** coverage by `1.10%`.
> The diff coverage is `61.76%`.

[![Impacted file tree graph](https://codecov.io/gh/20c/munge/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=5BzyJsbQ7d&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c)](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c)

```diff
@@            Coverage Diff             @@
##           master      #15      +/-   ##
==========================================
- Coverage   83.05%   81.94%   -1.11%     
==========================================
  Files          13       14       +1     
  Lines         472      493      +21     
==========================================
+ Hits          392      404      +12     
- Misses         80       89       +9     
```


| [Impacted Files](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c) | Coverage Δ | |
|---|---|---|
| [src/munge/codec/toml\_tomlkit.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2NvZGVjL3RvbWxfdG9tbGtpdC5weQ==) | `25.00% <25.00%> (ø)` | |
| [src/munge/codec/\_\_init\_\_.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2NvZGVjL19faW5pdF9fLnB5) | `97.77% <50.00%> (ø)` | |
| [src/munge/codec/toml.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2NvZGVjL3RvbWwucHk=) | `50.00% <50.00%> (-34.22%)` | :arrow_down: |
| [src/munge/codec/yaml.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2NvZGVjL3lhbWwucHk=) | `84.21% <81.25%> (ø)` | |
| [src/munge/codec/toml\_toml.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2NvZGVjL3RvbWxfdG9tbC5weQ==) | `84.21% <84.21%> (ø)` | |
| [src/munge/base.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2Jhc2UucHk=) | `84.61% <100.00%> (+0.30%)` | :arrow_up: |
| [src/munge/codec/json.py](https://codecov.io/gh/20c/munge/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c#diff-c3JjL211bmdlL2NvZGVjL2pzb24ucHk=) | `88.88% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c). Last update [882070d...716b825](https://codecov.io/gh/20c/munge/pull/15?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=20c).

lgtm-com[bot]: This pull request **introduces 3 alerts** when merging 716b825f84f9e10191342a8f0f722e10ef096f6e into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-f607610b86906a14ad80056a38c44879c6062a11)

**new alerts:**

* 3 for Unused import
lgtm-com[bot]: This pull request **introduces 3 alerts** and **fixes 1** when merging 7c3d4f3836c289ae05ec9193b92707eb5337fa8d into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-a36aed7947711b9c1dbcda16d06fc3136e112306)

**new alerts:**

* 3 for Unused import

**fixed alerts:**

* 1 for Unused import
lgtm-com[bot]: This pull request **introduces 3 alerts** and **fixes 1** when merging 5d229691d2eb82a6f5c63f04ae83d0c9a92dc067 into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-7d52adb374215ea7ec747a102c9b5a726927ab49)

**new alerts:**

* 3 for Unused import

**fixed alerts:**

* 1 for Unused import
lgtm-com[bot]: This pull request **introduces 3 alerts** and **fixes 1** when merging b8f6250131b6fcc14277618850495f73749d7043 into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-c0983bc255bd32091fe682120620b915ef8d8332)

**new alerts:**

* 3 for Unused import

**fixed alerts:**

* 1 for Unused import
lgtm-com[bot]: This pull request **introduces 2 alerts** and **fixes 1** when merging ddebdac91a19681ff229d8aaec5d53f3b122542b into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-acd34e70c1c474773ea2b3fc6dff8f71dc48087f)

**new alerts:**

* 2 for Unused import

**fixed alerts:**

* 1 for Unused import
lgtm-com[bot]: This pull request **introduces 2 alerts** and **fixes 1** when merging b83f3e2a80d456f8bf554887f659c67f506bdf41 into 882070dbec6a3600217e406c9501091d8c27feab - [view on LGTM.com](https://lgtm.com/projects/g/20c/munge/rev/pr-0f6630bb33ccdc6e76c60263a4995e61e0349975)

**new alerts:**

* 2 for Unused import

**fixed alerts:**

* 1 for Unused import

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index feee7f9..f2b20e7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,3 @@
-fail_fast: true
 repos:
   - repo: local
     hooks:
@@ -21,4 +20,12 @@ repos:
         name: Black
         entry: poetry run black .
         language: system
-        pass_filenames: false
\ No newline at end of file
+        pass_filenames: false
+# the namespace imports for meta all trigger
+#  - repo: local
+#    hooks:
+#      - id: system
+#        name: flake8
+#        entry: poetry run flake8 src/
+#        language: system
+#        pass_filenames: false
\ No newline at end of file
diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml
index 24ed2ba..6cf41c8 100644
--- a/CHANGELOG.yaml
+++ b/CHANGELOG.yaml
@@ -48,7 +48,10 @@ Unreleased:
   - download and repo urls to setup.py (#10)
   - toml support
   - extras
-  changed: []
+  - roundtrip support
+  - support for typed codecs (#3)
+  changed:
+  - don't sort keys by default in yaml
   deprecated: []
   fixed: []
   removed: []
diff --git a/poetry.lock b/poetry.lock
index 1215c9a..a48625f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -66,14 +66,38 @@ typing-extensions = ">=3.7.4"
 colorama = ["colorama (>=0.4.3)"]
 d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
 
+[[package]]
+name = "bleach"
+version = "3.3.0"
+description = "An easy safelist-based HTML-sanitizing tool."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+packaging = "*"
+six = ">=1.9.0"
+webencodings = "*"
+
 [[package]]
 name = "certifi"
-version = "2020.12.5"
+version = "2021.5.30"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "cffi"
+version = "1.14.5"
+description = "Foreign Function Interface for Python calling C code."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
 [[package]]
 name = "cfgv"
 version = "3.0.0"
@@ -122,6 +146,14 @@ category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
+[[package]]
+name = "confu"
+version = "1.7.1"
+description = "Configuration file validation and generation"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
 [[package]]
 name = "coverage"
 version = "5.5"
@@ -130,11 +162,42 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
 
+[package.extras]
+toml = ["toml"]
+
+[[package]]
+name = "cryptography"
+version = "3.4.7"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
 [package.dependencies]
-toml = {version = "*", optional = true, markers = "extra == \"toml\""}
+cffi = ">=1.12"
 
 [package.extras]
-toml = ["toml"]
+docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
+docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
+pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
+sdist = ["setuptools-rust (>=0.11.4)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
+
+[[package]]
+name = "ctl"
+version = "1.0.0"
+description = "Full control of your environment"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+confu = ">=1.4,<2.0"
+git-url-parse = ">=1.1,<2.0"
+grainy = ">=1.4,<2.0"
+munge = ">=1,<2"
+pluginmgr = ">=1,<2"
 
 [[package]]
 name = "dataclasses"
@@ -146,12 +209,20 @@ python-versions = ">=3.6, <3.7"
 
 [[package]]
 name = "distlib"
-version = "0.3.1"
+version = "0.3.2"
 description = "Distribution utilities"
 category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "docutils"
+version = "0.17.1"
+description = "Docutils -- Python Documentation Utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
 [[package]]
 name = "filelock"
 version = "3.0.12"
@@ -174,6 +245,25 @@ mccabe = ">=0.6.0,<0.7.0"
 pycodestyle = ">=2.7.0,<2.8.0"
 pyflakes = ">=2.3.0,<2.4.0"
 
+[[package]]
+name = "future"
+version = "0.18.2"
+description = "Clean single-source support for Python 3 and 2"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "git-url-parse"
+version = "1.2.2"
+description = "git-url-parse - A simple GIT URL parser."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pbr = "*"
+
 [[package]]
 name = "gitdb"
 version = "4.0.7"
@@ -187,16 +277,24 @@ smmap = ">=3.0.1,<5"
 
 [[package]]
 name = "gitpython"
-version = "3.1.17"
+version = "3.1.18"
 description = "Python Git Library"
 category = "dev"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
 typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""}
 
+[[package]]
+name = "grainy"
+version = "1.8.1"
+description = "granular permissions utility"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
 [[package]]
 name = "identify"
 version = "1.6.2"
@@ -218,7 +316,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.3.1"
+version = "4.5.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -268,6 +366,57 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
 requirements_deprecated_finder = ["pipreqs", "pip-api"]
 colors = ["colorama (>=0.4.3,<0.5.0)"]
 
+[[package]]
+name = "jeepney"
+version = "0.6.0"
+description = "Low-level, pure Python DBus protocol wrapper."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio"]
+
+[[package]]
+name = "jinja2"
+version = "2.11.3"
+description = "A very fast and expressive template engine."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+MarkupSafe = ">=0.23"
+
+[package.extras]
+i18n = ["Babel (>=0.8)"]
+
+[[package]]
+name = "keyring"
+version = "23.0.1"
+description = "Store and access your passwords safely."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = ">=3.6"
+jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
+pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""}
+SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[[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"
@@ -335,6 +484,17 @@ category = "dev"
 optional = false
 python-versions = ">=2.6"
 
+[[package]]
+name = "pkginfo"
+version = "1.7.0"
+description = "Query metadatdata from sdists / bdists / installed packages."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+testing = ["nose", "coverage"]
+
 [[package]]
 name = "pluggy"
 version = "0.13.1"
@@ -349,6 +509,17 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 [package.extras]
 dev = ["pre-commit", "tox"]
 
+[[package]]
+name = "pluginmgr"
+version = "1.0.1"
+description = "lightweight python plugin system supporting config inheritance"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+munge = ">=1.0.0,<2.0.0"
+
 [[package]]
 name = "pre-commit"
 version = "2.1.1"
@@ -383,6 +554,14 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pycparser"
+version = "2.20"
+description = "C parser in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
 [[package]]
 name = "pyflakes"
 version = "2.3.1"
@@ -391,6 +570,14 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pygments"
+version = "2.9.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
 [[package]]
 name = "pyparsing"
 version = "2.4.7"
@@ -423,18 +610,19 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
 
 [[package]]
 name = "pytest-cov"
-version = "2.12.0"
+version = "2.12.1"
 description = "Pytest plugin for measuring coverage."
 category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [package.dependencies]
-coverage = {version = ">=5.2.1", extras = ["toml"]}
+coverage = ">=5.2.1"
 pytest = ">=4.6"
+toml = "*"
 
 [package.extras]
-testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
 
 [[package]]
 name = "pyupgrade"
@@ -447,6 +635,14 @@ python-versions = ">=3.6"
 [package.dependencies]
 tokenize-rt = ">=3.2.0"
 
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "pyyaml"
 version = "5.4.1"
@@ -455,6 +651,23 @@ category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
+[[package]]
+name = "readme-renderer"
+version = "29.0"
+description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+bleach = ">=2.1.0"
+docutils = ">=0.13.1"
+Pygments = ">=2.5.1"
+six = "*"
+
+[package.extras]
+md = ["cmarkgfm (>=0.5.0,<0.6.0)"]
+
 [[package]]
 name = "regex"
 version = "2021.4.4"
@@ -481,6 +694,40 @@ urllib3 = ">=1.21.1,<1.27"
 security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
 socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
 
+[[package]]
+name = "requests-toolbelt"
+version = "0.9.1"
+description = "A utility belt for advanced users of python-requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=2.0.1,<3.0.0"
+
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "secretstorage"
+version = "3.3.1"
+description = "Python bindings to FreeDesktop.org Secret Service API"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cryptography = ">=2.0"
+jeepney = ">=0.6"
+
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -509,6 +756,17 @@ python-versions = ">=3.6"
 importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""}
 pbr = ">=2.0.0,<2.1.0 || >2.1.0"
 
+[[package]]
+name = "tmpl"
+version = "0.3.0"
+description = "template abstraction and helper functions"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+future = "*"
+
 [[package]]
 name = "tokenize-rt"
 version = "3.2.0"
@@ -525,6 +783,14 @@ category = "main"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
+[[package]]
+name = "tomlkit"
+version = "0.7.2"
+description = "Style preserving TOML library"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
 [[package]]
 name = "tox"
 version = "3.23.1"
@@ -548,6 +814,38 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,
 docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
 testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"]
 
+[[package]]
+name = "tqdm"
+version = "4.61.1"
+description = "Fast, Extensible Progress Meter"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+telegram = ["requests"]
+
+[[package]]
+name = "twine"
+version = "3.4.1"
+description = "Collection of utilities for publishing packages on PyPI"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = ">=0.4.3"
+importlib-metadata = ">=3.6"
+keyring = ">=15.1"
+pkginfo = ">=1.4.2"
+readme-renderer = ">=21.0"
+requests = ">=2.20"
+requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
+rfc3986 = ">=1.4.0"
+tqdm = ">=4.14"
+
 [[package]]
 name = "typed-ast"
 version = "1.4.3"
@@ -597,6 +895,14 @@ six = ">=1.9.0,<2"
 docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
 testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
 
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "zipp"
 version = "3.4.1"
@@ -611,12 +917,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
 
 [extras]
 toml = ["toml"]
+tomlkit = ["tomlkit"]
 yaml = ["PyYAML"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "961b75ae9f24db41ba75ea1a5398152cbb8020c68eb38799bd6deeb302f19154"
+content-hash = "58066456f0b29d7e3aaa07220e88fecd984703a8d2b458d92ccdbf69190cb35a"
 
 [metadata.files]
 appdirs = [
@@ -638,9 +945,52 @@ bandit = [
 black = [
     {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
 ]
+bleach = [
+    {file = "bleach-3.3.0-py2.py3-none-any.whl", hash = "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125"},
+    {file = "bleach-3.3.0.tar.gz", hash = "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"},
+]
 certifi = [
-    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
-    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+    {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
+    {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
+]
+cffi = [
+    {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"},
+    {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"},
+    {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"},
+    {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"},
+    {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"},
+    {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"},
+    {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"},
+    {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"},
+    {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"},
+    {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"},
+    {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"},
+    {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"},
+    {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
+    {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
+    {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
+    {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
+    {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
+    {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
+    {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
+    {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
+    {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
+    {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
+    {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
+    {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
+    {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
 ]
 cfgv = [
     {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"},
@@ -663,6 +1013,9 @@ colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
+confu = [
+    {file = "confu-1.7.1.tar.gz", hash = "sha256:b2302fac738ee23143fe1768053a1c63216928fa54d1bba2326282d1c0fe6de9"},
+]
 coverage = [
     {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"},
     {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"},
@@ -717,13 +1070,35 @@ coverage = [
     {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
     {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
 ]
+cryptography = [
+    {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"},
+    {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"},
+    {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"},
+    {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"},
+    {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"},
+    {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"},
+    {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"},
+    {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"},
+    {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"},
+    {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"},
+    {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"},
+    {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"},
+]
+ctl = [
+    {file = "ctl-1.0.0-py3-none-any.whl", hash = "sha256:776d490062c35dd413c929e7f7b4829142b4bf2cc03ddd9002899c0f7da1efaa"},
+    {file = "ctl-1.0.0.tar.gz", hash = "sha256:5e39e9e9505db18e1cd1ecf1f64530e7e900f625e4542545d66b3ad954e003b6"},
+]
 dataclasses = [
     {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
     {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
 ]
 distlib = [
-    {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
-    {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
+    {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"},
+    {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"},
+]
+docutils = [
+    {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
+    {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
 ]
 filelock = [
     {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
@@ -733,13 +1108,24 @@ flake8 = [
     {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
 ]
+future = [
+    {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
+git-url-parse = [
+    {file = "git-url-parse-1.2.2.tar.gz", hash = "sha256:7b5f4e3aeb1d693afeee67a3bd4ac063f7206c2e8e46e559f0da0da98445f117"},
+    {file = "git_url_parse-1.2.2-py2-none-any.whl", hash = "sha256:9353ff40d69488ff2299b27f40e0350ad87bd5348ea6ea09a1895eda9e5733de"},
+    {file = "git_url_parse-1.2.2-py3-none-any.whl", hash = "sha256:4655ee22f1d8bf7a1eb1066c1da16529b186966c6d8331f7f55686a76a9f7aef"},
+]
 gitdb = [
     {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"},
     {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"},
 ]
 gitpython = [
-    {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"},
-    {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"},
+    {file = "GitPython-3.1.18-py3-none-any.whl", hash = "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"},
+    {file = "GitPython-3.1.18.tar.gz", hash = "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b"},
+]
+grainy = [
+    {file = "grainy-1.8.1.tar.gz", hash = "sha256:2cfd8d50b3f5cce3c463f3c5e86324442f61a7cd46dfe7b134ee926559e56556"},
 ]
 identify = [
     {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"},
@@ -750,8 +1136,8 @@ idna = [
     {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-4.3.1-py3-none-any.whl", hash = "sha256:c2e27fa8b6c8b34ebfcd4056ae2ca290e36250d1fbeceec85c1c67c711449fac"},
-    {file = "importlib_metadata-4.3.1.tar.gz", hash = "sha256:2d932ea08814f745863fd20172fe7de4794ad74567db78f2377343e24520a5b6"},
+    {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"},
+    {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"},
 ]
 importlib-resources = [
     {file = "importlib_resources-5.1.4-py3-none-any.whl", hash = "sha256:e962bff7440364183203d179d7ae9ad90cb1f2b74dcb84300e88ecc42dca3351"},
@@ -765,6 +1151,54 @@ isort = [
     {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"},
     {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"},
 ]
+jeepney = [
+    {file = "jeepney-0.6.0-py3-none-any.whl", hash = "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae"},
+    {file = "jeepney-0.6.0.tar.gz", hash = "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657"},
+]
+jinja2 = [
+    {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
+    {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
+]
+keyring = [
+    {file = "keyring-23.0.1-py3-none-any.whl", hash = "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"},
+    {file = "keyring-23.0.1.tar.gz", hash = "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8"},
+]
+markupsafe = [
+    {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-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-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_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-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-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"},
@@ -813,10 +1247,18 @@ pbr = [
     {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"},
     {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"},
 ]
+pkginfo = [
+    {file = "pkginfo-1.7.0-py2.py3-none-any.whl", hash = "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75"},
+    {file = "pkginfo-1.7.0.tar.gz", hash = "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4"},
+]
 pluggy = [
     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
 ]
+pluginmgr = [
+    {file = "pluginmgr-1.0.1-py3-none-any.whl", hash = "sha256:1f24786c2cac55e17f3496bd915b8c336792ecc9edb2b61ea5f73ce3a8a1eec9"},
+    {file = "pluginmgr-1.0.1.tar.gz", hash = "sha256:151c8afe8df535f2e02255433cd3340000b3428018692f0581ff53d6f74c3630"},
+]
 pre-commit = [
     {file = "pre_commit-2.1.1-py2.py3-none-any.whl", hash = "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6"},
     {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"},
@@ -829,10 +1271,18 @@ pycodestyle = [
     {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
     {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
 ]
+pycparser = [
+    {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
+    {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
+]
 pyflakes = [
     {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
     {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
 ]
+pygments = [
+    {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
+    {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
+]
 pyparsing = [
     {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@@ -842,13 +1292,17 @@ pytest = [
     {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
 ]
 pytest-cov = [
-    {file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"},
-    {file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"},
+    {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
+    {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
 ]
 pyupgrade = [
     {file = "pyupgrade-2.0.2-py2.py3-none-any.whl", hash = "sha256:3d81ecec17cc6be0db2aca48d3756f4542ecfaa18855d3dfa2962ac6425db9f3"},
     {file = "pyupgrade-2.0.2.tar.gz", hash = "sha256:5f26bfc17402f612a675c181f3e7d07542b519d9df67e5d01704c203d0c79523"},
 ]
+pywin32-ctypes = [
+    {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
+    {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+]
 pyyaml = [
     {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
     {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -872,6 +1326,10 @@ pyyaml = [
     {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
     {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
 ]
+readme-renderer = [
+    {file = "readme_renderer-29.0-py2.py3-none-any.whl", hash = "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c"},
+    {file = "readme_renderer-29.0.tar.gz", hash = "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"},
+]
 regex = [
     {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"},
     {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"},
@@ -919,6 +1377,18 @@ requests = [
     {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
     {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
 ]
+requests-toolbelt = [
+    {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
+    {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
+]
+rfc3986 = [
+    {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
+    {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
+]
+secretstorage = [
+    {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"},
+    {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"},
+]
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -931,6 +1401,9 @@ stevedore = [
     {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"},
     {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"},
 ]
+tmpl = [
+    {file = "tmpl-0.3.0.tar.gz", hash = "sha256:b334f7f1ead0212ea3a55d6548adcb189a236d68eba5d45f199a73dfdf6b075e"},
+]
 tokenize-rt = [
     {file = "tokenize_rt-3.2.0-py2.py3-none-any.whl", hash = "sha256:53f5c22d36e5c6f8e3fdbc6cb4dd151d1b3d38cea1b85b5fef6268f153733899"},
     {file = "tokenize_rt-3.2.0.tar.gz", hash = "sha256:2f44eee8f620102f8a03c50142795121faf86e020d208896ea7a7047bbe933cf"},
@@ -939,10 +1412,22 @@ toml = [
     {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
+tomlkit = [
+    {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"},
+    {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"},
+]
 tox = [
     {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"},
     {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"},
 ]
+tqdm = [
+    {file = "tqdm-4.61.1-py2.py3-none-any.whl", hash = "sha256:aa0c29f03f298951ac6318f7c8ce584e48fa22ec26396e6411e43d038243bdb2"},
+    {file = "tqdm-4.61.1.tar.gz", hash = "sha256:24be966933e942be5f074c29755a95b315c69a91f839a29139bf26ffffe2d3fd"},
+]
+twine = [
+    {file = "twine-3.4.1-py3-none-any.whl", hash = "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83"},
+    {file = "twine-3.4.1.tar.gz", hash = "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b"},
+]
 typed-ast = [
     {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
     {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
@@ -988,6 +1473,10 @@ virtualenv = [
     {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"},
     {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"},
 ]
+webencodings = [
+    {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+    {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
 zipp = [
     {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"},
     {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"},
diff --git a/pyproject.toml b/pyproject.toml
index c917ec0..6256ba6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,6 +2,8 @@
 name = "munge"
 version = "1.1.0"
 description = "data manipulation library and client"
+readme = "README.md"
+repository = "https://github.com/20c/munge/"
 authors = ["20C <code@20c.com>"]
 license = "Apache-2.0"
 
@@ -28,7 +30,7 @@ click = "^8.0.1"
 
 PyYAML = {version = "^5.1", optional = true}
 toml = {version = "^0.10.2", optional = true}
-# tomlkit = {version = "^0.7.2", optional = true}
+tomlkit = {version = "^0.7.2", optional = true}
 
 [tool.poetry.dev-dependencies]
 pytest = "*"
@@ -46,8 +48,15 @@ pre-commit = "^2"
 mypy = "^0.812"
 pyupgrade = "^2"
 
+# ctl
+ctl = "^1.0.0"
+jinja2 = "^2.11.2"
+tmpl = "^0.3.0"
+twine = "^3.3.0"
+
 [tool.poetry.extras]
 toml = ["toml"]
+tomlkit = ["tomlkit"]
 yaml = ["PyYAML"]
 
 [build-system]
diff --git a/src/munge/__init__.py b/src/munge/__init__.py
index de47847..13812b7 100644
--- a/src/munge/__init__.py
+++ b/src/munge/__init__.py
@@ -5,5 +5,8 @@ from .codec import get_codecs  # noqa
 from .codec import load_datafile  # noqa
 from .config import Config
 
-if "MUNGE_EXPLICIT_IMPORT" not in globals():
+if not globals().get("MUNGE_EXPLICIT_IMPORT", False):
     from .codec import all  # noqa
+else:
+    print(globals())
+    assert 0
diff --git a/src/munge/base.py b/src/munge/base.py
index 01dbdd0..5f70168 100644
--- a/src/munge/base.py
+++ b/src/munge/base.py
@@ -8,6 +8,8 @@ from munge import codec
 
 
 class Meta(type):
+    """Metadata class to check and register codec classes."""
+
     def __init__(cls, name, bases, attrs):
         if name == "CodecBase":
             super().__init__(name, bases, attrs)
@@ -30,6 +32,7 @@ class Meta(type):
 class CodecBase(metaclass=Meta):
     supports_dict = False
     supports_list = False
+    supports_roundtrip = False
 
     def __init__(self, config=None):
         if config:
diff --git a/src/munge/cli.py b/src/munge/cli.py
index 27acc85..fef8676 100644
--- a/src/munge/cli.py
+++ b/src/munge/cli.py
@@ -1,5 +1,3 @@
-import sys
-
 import click
 
 import munge
diff --git a/src/munge/codec/__init__.py b/src/munge/codec/__init__.py
index 4964205..3d51a9d 100644
--- a/src/munge/codec/__init__.py
+++ b/src/munge/codec/__init__.py
@@ -11,7 +11,7 @@ def add_codec(exts, cls):
     # check for dupe extensions
     dupe_exts = {ext for k in list(__codecs.keys()) for ext in k}.intersection(exts)
     if dupe_exts:
-        raise ValueError("duplicate extension %s" % str(dupe_exts))
+        raise ValueError(f"duplicate extension {str(dupe_exts)}")
 
     __codecs[exts] = cls
 
@@ -75,9 +75,7 @@ def load_datafile(name, search_path=("."), codecs=get_codecs(), **kwargs):
     if not mod:
         if "default" in kwargs:
             return kwargs["default"]
-        raise OSError(
-            "file {} not found in search path {}".format(name, str(search_path))
-        )
+        raise OSError(f"file {name} not found in search path {str(search_path)}")
 
     (codec, datafile) = mod[0]
     return codec().load(open(datafile))
diff --git a/src/munge/codec/django.py b/src/munge/codec/django.py
deleted file mode 100644
index d7d67cc..0000000
--- a/src/munge/codec/django.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from munge.base import CodecBase
-
-# FIXME - remove this file?
-# try:
-#    pass
-#    # from django.conf import settings
-#
-# except ImportError as exc:
-#    pass
-
-
-class Django(CodecBase):
-
-    extensions = ["django"]
-    __kwargs = {}
-
-    def load(self, fobj):
-        raise NotImplementedError()
-
-    def loads(self, instr):
-        raise NotImplementedError()
-
-    def dump(self, data, fobj):
-        raise NotImplementedError()
-
-    def dumps(self, data):
-        raise NotImplementedError()
diff --git a/src/munge/codec/json.py b/src/munge/codec/json.py
index 4d4f8e8..a70db89 100644
--- a/src/munge/codec/json.py
+++ b/src/munge/codec/json.py
@@ -17,8 +17,8 @@ class Json(CodecBase):
     def load(self, fobj, **kwargs):
         return json.load(fobj, **self.__kwargs)
 
-    def loads(self, instr, **kwargs):
-        return json.loads(instr, **self.__kwargs)
+    def loads(self, input_string, **kwargs):
+        return json.loads(input_string, **self.__kwargs)
 
     def dump(self, data, fobj, **kwargs):
         return json.dump(data, fobj, **kwargs)
diff --git a/src/munge/codec/toml.py b/src/munge/codec/toml.py
index 43faca3..9fb4e61 100644
--- a/src/munge/codec/toml.py
+++ b/src/munge/codec/toml.py
@@ -1,28 +1,16 @@
-from munge.base import CodecBase
+import importlib
+import os
 
 try:
-    import toml
-
+    if os.environ.get("MUNGE_TOML_LIBRARY", None):
+        importlib.import_module(f"munge.codec.toml_{os.environ['MUNGE_TOML_LIBRARY']}")
+    else:
+        import munge.codec.toml_tomlkit  # noqa isort:skip
+        import munge.codec.toml_toml  # noqa
+
+except ValueError as exc:
+    # don't load both toml modules
+    if str(exc).startswith("duplicate extension"):
+        pass
 except ImportError:
     pass
-
-
-class Toml(CodecBase):
-    supports_dict = True
-    extensions = ["toml"]
-    __kwargs = {}
-
-    def set_type(self, name, typ):
-        pass
-
-    def load(self, fobj, **kwargs):
-        return toml.load(fobj, **self.__kwargs)
-
-    def loads(self, input_string, **kwargs):
-        return toml.loads(input_string, **self.__kwargs)
-
-    def dump(self, data, fobj, **kwargs):
-        return toml.dump(data, fobj, **kwargs)
-
-    def dumps(self, data):
-        return toml.dumps(data)
diff --git a/src/munge/codec/toml_toml.py b/src/munge/codec/toml_toml.py
new file mode 100644
index 0000000..0927bb4
--- /dev/null
+++ b/src/munge/codec/toml_toml.py
@@ -0,0 +1,28 @@
+from munge.base import CodecBase
+
+try:
+    import toml
+
+    class Toml(CodecBase):
+        supports_dict = True
+        extensions = ["toml"]
+        __kwargs = {}
+
+        def set_type(self, name, typ):
+            pass
+
+        def load(self, fobj, **kwargs):
+            return toml.load(fobj, **self.__kwargs)
+
+        def loads(self, input_string, **kwargs):
+            return toml.loads(input_string, **self.__kwargs)
+
+        def dump(self, data, fobj, **kwargs):
+            return toml.dump(data, fobj, **kwargs)
+
+        def dumps(self, data):
+            return toml.dumps(data)
+
+
+except ImportError:
+    pass
diff --git a/src/munge/codec/toml_tomlkit.py b/src/munge/codec/toml_tomlkit.py
new file mode 100644
index 0000000..f18b189
--- /dev/null
+++ b/src/munge/codec/toml_tomlkit.py
@@ -0,0 +1,29 @@
+from munge.base import CodecBase
+
+try:
+    import tomlkit
+
+    class TomlKit(CodecBase):
+        supports_dict = True
+        supports_roundtrip = True
+        extensions = ["toml"]
+        __kwargs = {}
+
+        def set_type(self, name, typ):
+            pass
+
+        def load(self, fobj, **kwargs):
+            return self.loads(fobj.read(), **self.__kwargs)
+
+        def loads(self, input_string, **kwargs):
+            return tomlkit.loads(input_string, **self.__kwargs)
+
+        def dump(self, data, fobj, **kwargs):
+            return fobj.write(self.dumps(data, **kwargs))
+
+        def dumps(self, data, **kwargs):
+            return tomlkit.dumps(data)
+
+
+except ImportError:
+    pass
diff --git a/src/munge/codec/yaml.py b/src/munge/codec/yaml.py
index 229ac10..aeac8cb 100644
--- a/src/munge/codec/yaml.py
+++ b/src/munge/codec/yaml.py
@@ -3,26 +3,28 @@ from munge.base import CodecBase
 try:
     import yaml
 
-except ImportError:
-    pass
+    class Yaml(CodecBase):
+        supports_dict = True
+        supports_list = True
+        extensions = ["yaml", "yml"]
 
+        def set_type(self, name, typ):
+            pass
 
-class Yaml(CodecBase):
-    supports_dict = True
-    supports_list = True
-    extensions = ["yaml", "yml"]
+        def load(self, *args, **kwargs):
+            return yaml.safe_load(*args, **kwargs)
 
-    def set_type(self, name, typ):
-        pass
+        def loads(self, *args, **kwargs):
+            return self.load(*args, **kwargs)
 
-    def load(self, *args, **kwargs):
-        return yaml.safe_load(*args, **kwargs)
+        def dump(self, data, fobj):
+            return fobj.write(
+                yaml.safe_dump(data, default_flow_style=False, sort_keys=False)
+            )
 
-    def loads(self, *args, **kwargs):
-        return self.load(*args, **kwargs)
+        def dumps(self, data):
+            return yaml.safe_dump(data, default_flow_style=False, sort_keys=False)
 
-    def dump(self, data, fobj):
-        return fobj.write(yaml.safe_dump(data, default_flow_style=False))
 
-    def dumps(self, data):
-        return yaml.safe_dump(data, default_flow_style=False)
+except ImportError:
+    pass
diff --git a/tox.ini b/tox.ini
index aea00ae..e3562af 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,3 +1,18 @@
+[flake8]
+extend-ignore = E501
+exclude = [
+    .git,
+    .venv,
+    .tox,
+    __pycache__,
+    build,
+    dist
+  ]
+
+max-line-length = 80
+max-complexity = 18
+select = B,C,E,F,W,T4,B9
+
 
 [pytest]
 norecursedirs = .facsimile data gen .tox
@@ -15,4 +30,4 @@ setenv =
 deps = -r{toxinidir}/Ctl/requirements.txt
        -r{toxinidir}/Ctl/requirements-test.txt
 commands =
-    py.test -vv --cov-report=term-missing --cov={envsitepackagesdir}/munge tests/
+    pytest -vv --cov-report=term-missing --cov={envsitepackagesdir}/munge tests/


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 93ac1a6..22e2143 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -71,8 +71,8 @@ jobs:
           key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
       # install dependencies if cache does not exist
       - name: Check cache and install dependencies
-        run: poetry install
-        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
+        run: poetry install -E tomlkit -E yaml
+# tmp disable cache        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
       - name: Run tests
         run: |
           source .venv/bin/activate
diff --git a/tests/data/conf0/config.toml b/tests/data/conf0/config.toml
new file mode 100644
index 0000000..a775602
--- /dev/null
+++ b/tests/data/conf0/config.toml
@@ -0,0 +1,5 @@
+[addrbook.site0]
+url = "https://example.com/data.json"
+user = "user"
+password = "secr3t"
+timeout = 60
diff --git a/tests/data/conf0/config.yaml b/tests/data/conf0/config.yaml
deleted file mode 100644
index cc6fc98..0000000
--- a/tests/data/conf0/config.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-addrbook:
-  site0:
-    url: https://example.com/data.json
-    user: user
-    password: secr3t
-    timeout: 60
-
diff --git a/tests/data/dict0.toml b/tests/data/dict0.toml
index 8816b8e..f8d09bf 100644
--- a/tests/data/dict0.toml
+++ b/tests/data/dict0.toml
@@ -1,4 +1,5 @@
 [munge]
-list0 = [ "item0", "item1",]
-int0 = 42
+# list0
+list0 = ["item0", "item1"]
+int0 = 42 # the answer to everything
 str0 = "str0"
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 44c9201..d660dac 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -18,5 +18,5 @@ def test_cli():
     rv = runner.invoke(munge.cli.main, ["--list-codecs"])
     assert 0 == rv.exit_code
 
-    rv = runner.invoke(munge.cli.main, [os.path.join(data_dir, "dict0.yml"), "json:"])
+    rv = runner.invoke(munge.cli.main, [os.path.join(data_dir, "dict0.toml"), "json:"])
     assert 0 == rv.exit_code
diff --git a/tests/test_codecs.py b/tests/test_codecs.py
index 4b44284..6bb34c5 100644
--- a/tests/test_codecs.py
+++ b/tests/test_codecs.py
@@ -14,6 +14,7 @@ data_dir = os.path.join(this_dir, "data")
 test_codecs = []
 for tags, cls in list(munge.get_codecs().items()):
     if any(name in ("json", "toml", "yaml") for name in tags):
+        print(f"appending codec {cls.extension}")
         test_codecs.append(cls)
 
 
@@ -134,14 +135,34 @@ def test_loadu(codec, dataset):
 
 
 def test_dump(codec, dataset, tmpdir):
-    # XXX normalize
     obj = codec.cls()
     if not obj.supports_data(dataset.expected):
         return
     dstfile = tmpdir.join("dump" + obj.extension)
     obj.dump(dataset.expected, dstfile.open("w"))
-    assert dataset.expected == obj.load(dstfile.open())
-    # assert codec.open_file(dataset.filename).read() == dstfile.read()
+    with dstfile.open() as fobj:
+        assert dataset.expected == obj.load(fobj)
+
+
+def test_roundtrip(codec, dataset, tmpdir):
+    obj = codec.cls()
+    if not obj.supports_data(dataset.expected):
+        return
+    if not obj.supports_roundtrip:
+        return
+
+    data = obj.load(open(codec.find_file(dataset.filename)))
+    for section in dataset.expected:
+        for k, v in dataset.expected[section].items():
+            data[section][k] = v
+
+    dumped = obj.dumps(data)
+    print(f"dumping: {dumped}")
+
+    dstfile = tmpdir.join("dump" + obj.extension)
+    obj.dump(data, dstfile.open("w"))
+    with dstfile.open() as fobj:
+        assert codec.open_file(dataset.filename).read() == fobj.read()
 
 
 def test_dumps(codec, dataset, tmpdir):
@@ -190,7 +211,7 @@ def test_load_datafile(codec, dataset):
     if not obj.supports_data(dataset.expected):
         return
 
-    # XXX move the nonexistant tests to their own function so they're not repeatedly called
+    # TODO move the nonexistant tests to their own function so they're not repeatedly called
     with pytest.raises(IOError):
         munge.load_datafile("nonexistant", data_dir)
 
diff --git a/tests/test_config.py b/tests/test_config.py
index 7c5c2ad..6781609 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -13,39 +13,31 @@ from munge import config
 test_dir = os.path.relpath(os.path.dirname(__file__))
 data_dir = os.path.join(test_dir, "data")
 conf0_dir = os.path.join(data_dir, "conf0")
-extra_schemes = {"tyam": {"type": "yaml", "cls": munge.get_codec("yaml")}}
+extra_schemes = {"tyam": {"type": "toml", "cls": munge.get_codec("toml")}}
 
 
 def test_parse_url():
-    # django = munge.get_codec("django")
     mysql = munge.get_codec("mysql")
     json = munge.get_codec("json")
-    yaml = munge.get_codec("yaml")
+    toml = munge.get_codec("toml")
 
     # fail on empty
     with pytest.raises(ValueError):
         config.parse_url("")
 
-    conf = config.parse_url("yaml:test")
-    conf = config.parse_url("yaml:test")
-    assert yaml == conf.cls
+    conf = config.parse_url("toml:test")
+    conf = config.parse_url("toml:test")
+    assert toml == conf.cls
     assert "test" == conf.url.path
 
-    conf = config.parse_url("test.yaml")
-    assert yaml == conf.cls
-    assert "test.yaml" == conf.url.path
+    conf = config.parse_url("test.toml")
+    assert toml == conf.cls
+    assert "test.toml" == conf.url.path
 
     conf = config.parse_url("tyam:test", extra_schemes)
-    assert yaml == conf.cls
+    assert toml == conf.cls
     assert "test" == conf.url.path
 
-    #   conf = config.parse_url(
-    #       "django:///home/user/project/settings_dir.settings?app_name/model"
-    #   )
-    #   assert django == conf.cls
-    #   assert "/home/user/project/settings_dir.settings" == conf.url.path
-    #   assert "app_name/model" == conf.url.query
-
     conf = config.parse_url("json:http://example.com/test.txt")
     assert json == conf.cls
     assert "http://example.com/test.txt" == conf.url.path
@@ -70,7 +62,7 @@ conf0_data = {
 
 
 class DefaultConfig(munge.Config):
-    defaults = {"config": default_config, "config_dir": "~/.mungeX", "codec": "yaml"}
+    defaults = {"config": default_config, "config_dir": "~/.mungeX", "codec": "toml"}
 
     class Defaults:
         config = default_config
@@ -255,7 +247,7 @@ def test_config_write(conf, tmpdir):
         type(conf)().write()
 
     cdir = tmpdir.mkdir(type(conf).__name__)
-    conf.write(str(cdir), "yaml")
+    conf.write(str(cdir), "toml")
 
     # create an exact copy
     kwargs = conf._defaults.copy()
diff --git a/tests/test_globals.py b/tests/test_globals.py
new file mode 100644
index 0000000..69b4e5a
--- /dev/null
+++ b/tests/test_globals.py
@@ -0,0 +1,23 @@
+import importlib
+import os
+import sys
+
+
+def unload_munge():
+    for key in list(sys.modules.keys()):
+        if key.startswith("munge"):
+            del sys.modules[key]
+
+
+def test_toml_library():
+    import munge
+
+    codec = munge.get_codec("toml")
+    assert codec.supports_roundtrip
+
+    os.environ["MUNGE_TOML_LIBRARY"] = "toml"
+    unload_munge()
+    import munge
+
+    codec = munge.get_codec("toml")
+    assert not codec.supports_roundtrip