From 7b8a8c8b592fa3cb86ed289549fdf0502a4084bc Mon Sep 17 00:00:00 2001 From: Allan Almazan Date: Fri, 1 Sep 2023 13:24:26 -0700 Subject: [PATCH] Add library functions (#1) --- .github/workflows/main.yml | 53 ++-- .pre-commit-config.yaml | 11 + .python-version | 1 + Makefile | 2 - README.md | 70 ++++- docs/about/changelog.md | 6 +- docs/about/contributing.md | 94 +------ docs/about/license.md | 23 +- docs/index.md | 1 + docs/requirements.txt | 3 + examples/decrypt_payload.py | 38 +++ examples/keys/private.pem | 28 ++ examples/keys/public.pub | 9 + poetry.lock | 250 +++++++++++++++++- pyproject.toml | 6 +- python_anvil_encryption/__init__.py | 11 +- python_anvil_encryption/cli.py | 9 +- python_anvil_encryption/encryption.py | 149 +++++++++++ python_anvil_encryption/py.typed | 1 + python_anvil_encryption/tests/conftest.py | 32 +++ python_anvil_encryption/tests/data/aa.pem | 28 ++ python_anvil_encryption/tests/data/aa.pub | 9 + .../tests/data/aa_forge_complete_payload.json | 5 + .../tests/test_encryption.py | 83 ++++++ python_anvil_encryption/tests/test_utils.py | 12 - python_anvil_encryption/utils.py | 14 - tests/test_cli.py | 28 +- 27 files changed, 760 insertions(+), 216 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version mode change 100644 => 120000 docs/about/changelog.md mode change 100644 => 120000 docs/about/contributing.md mode change 100644 => 120000 docs/about/license.md create mode 120000 docs/index.md create mode 100644 docs/requirements.txt create mode 100644 examples/decrypt_payload.py create mode 100644 examples/keys/private.pem create mode 100644 examples/keys/public.pub create mode 100644 python_anvil_encryption/encryption.py create mode 100644 python_anvil_encryption/py.typed create mode 100644 python_anvil_encryption/tests/data/aa.pem create mode 100644 python_anvil_encryption/tests/data/aa.pub create mode 100644 python_anvil_encryption/tests/data/aa_forge_complete_payload.json create mode 100644 python_anvil_encryption/tests/test_encryption.py delete mode 100644 python_anvil_encryption/tests/test_utils.py delete mode 100644 python_anvil_encryption/utils.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d8d89d..153dabb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ name: main -on: [push] +on: [ push ] jobs: build: @@ -8,36 +8,41 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7'] + python-version: [ 3.7, 3.8, 3.9, "3.10" ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - name: Install poetry + run: pipx install poetry - - uses: Gr1N/setup-poetry@v8 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" - - name: Check dependencies - run: make doctor + - name: Install poetry dependencies + run: poetry install - - uses: actions/cache@v2 - with: - path: .venv - key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + - name: Check dependencies + run: make doctor - - name: Install dependencies - run: make install + - uses: actions/cache@v2 + with: + path: .venv + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} - - name: Check code - run: make check + - name: Install dependencies + run: make install - - name: Test code - run: make test + - name: Check code + run: make check - - name: Upload coverage - uses: codecov/codecov-action@v1 - with: - fail_ci_if_error: true + - name: Test code + run: make test + +# - name: Upload coverage +# uses: codecov/codecov-action@v1 +# with: +# fail_ci_if_error: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f231628 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..584a914 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.7.11 diff --git a/Makefile b/Makefile index 9505fa5..8296ecf 100644 --- a/Makefile +++ b/Makefile @@ -36,12 +36,10 @@ $(DEPENDENCIES): poetry.lock @ rm -rf ~/Library/Preferences/pypoetry @ poetry config virtualenvs.in-project true poetry install - @ touch $@ ifndef CI poetry.lock: pyproject.toml poetry lock --no-update - @ touch $@ endif .cache: diff --git a/README.md b/README.md index a57fb56..93c16f7 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,29 @@ -# Overview +![Horizontal Lockupblack](https://user-images.githubusercontent.com/293079/169453889-ae211c6c-7634-4ccd-8ca9-8970c2621b6f.png#gh-light-mode-only) +![Horizontal Lockup copywhite](https://user-images.githubusercontent.com/293079/169453892-895f637b-4633-4a14-b997-960c9e17579b.png#gh-dark-mode-only) -Sample project generated from Jace's Python Template. +# Anvil Encryption -This project was generated with [cookiecutter](https://github.com/audreyr/cookiecutter) using [jacebrowning/template-python](https://github.com/jacebrowning/template-python). - -[![Unix Build Status](https://img.shields.io/github/actions/workflow/status/anvilco/python-anvil-encryption/main.yml?branch=main&label=linux)](https://github.com/anvilco/python-anvil-encryption/actions) -[![Windows Build Status](https://img.shields.io/appveyor/ci/anvilco/python-anvil-encryption.svg?label=windows)](https://ci.appveyor.com/project/anvilco/python-anvil-encryption) -[![Coverage Status](https://img.shields.io/codecov/c/gh/anvilco/python-anvil-encryption)](https://codecov.io/gh/anvilco/python-anvil-encryption) -[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/anvilco/python-anvil-encryption.svg)](https://scrutinizer-ci.com/g/anvilco/python-anvil-encryption) [![PyPI License](https://img.shields.io/pypi/l/python-anvil-encryption.svg)](https://pypi.org/project/python-anvil-encryption) [![PyPI Version](https://img.shields.io/pypi/v/python-anvil-encryption.svg)](https://pypi.org/project/python-anvil-encryption) -[![PyPI Downloads](https://img.shields.io/pypi/dm/python-anvil-encryption.svg?color=orange)](https://pypistats.org/packages/python-anvil-encryption) + +This library is a small wrapper around the [`pypa/cryptography` library](https://cryptography.io/en/latest/). +It offers convenience methods for encrypting and decrypting arbitrary payloads in both AES and RSA. + +For use encrypting / decrypting payloads in Anvil, you can generate an RSA keypair from +your [organization's settings page](https://www.useanvil.com/docs/api/getting-started#encryption). + +[Anvil](www.useanvil.com/developers) provides easy APIs for all things paperwork. + +1. [PDF filling API](https://www.useanvil.com/products/pdf-filling-api/) - fill out a PDF template with a web request + and structured JSON data. +2. [PDF generation API](https://www.useanvil.com/products/pdf-generation-api/) - send markdown or HTML and Anvil will + render it to a PDF. +3. [Etch E-sign with API](https://www.useanvil.com/products/etch/) - customizable, embeddable, e-signature platform with + an API to control the signing process end-to-end. +4. [Anvil Workflows (w/ API)](https://www.useanvil.com/products/workflows/) - Webforms + PDF + E-sign with a powerful + no-code builder. Easily collect structured data, generate PDFs, and request signatures. + +Learn more about Anvil on our [Anvil developer page](https://www.useanvil.com/developers). ## Setup @@ -34,10 +47,39 @@ $ poetry add python-anvil-encryption ## Usage -After installation, the package can be imported: +This usage example is also in a runnable form in the `examples/` directory. -```text -$ python ->>> import python-anvil-encryption ->>> python-anvil-encryption.__version__ +```python +import os +from python_anvil_encryption import encryption + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) + +# Keys could be read from a file (or preferably from environment variables) +public_key = None +private_key = None +with open(os.path.join(CURRENT_PATH, "keys/public.pub"), "rb") as pub_file: + public_key = pub_file.read() + +with open(os.path.join(CURRENT_PATH, "./keys/private.pem"), "rb") as priv_file: + private_key = priv_file.read() + +# RSA +message = b"Super secret message" +encrypted_message = encryption.encrypt_rsa(public_key, message) +decrypted_message = encryption.decrypt_rsa(private_key, encrypted_message) +assert decrypted_message == message +print(f"Are equal? {decrypted_message == message}") + +# AES +aes_key = encryption.generate_aes_key() +aes_encrypted_message = encryption.encrypt_aes(aes_key, message) +# The aes key in the first parameter is required to be in a hex +# byte string format. +decrypted_message = encryption.decrypt_aes( + aes_key.hex().encode(), + aes_encrypted_message +) +assert decrypted_message == message +print(f"Are equal? {decrypted_message == message}") ``` diff --git a/docs/about/changelog.md b/docs/about/changelog.md deleted file mode 100644 index d768f5c..0000000 --- a/docs/about/changelog.md +++ /dev/null @@ -1,5 +0,0 @@ -# Release Notes - -## 0.0.0 (YYYY-MM-DD) - - - TBD diff --git a/docs/about/changelog.md b/docs/about/changelog.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/docs/about/changelog.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/docs/about/contributing.md b/docs/about/contributing.md deleted file mode 100644 index dbf0236..0000000 --- a/docs/about/contributing.md +++ /dev/null @@ -1,93 +0,0 @@ -# Contributor Guide - -## Setup - -### Requirements - -* Make: - - macOS: `$ xcode-select --install` - - Linux: [https://www.gnu.org](https://www.gnu.org/software/make) - - Windows: `$ choco install make` [https://chocolatey.org](https://chocolatey.org/install) -* Python: `$ asdf install` (https://asdf-vm.com)[https://asdf-vm.com/guide/getting-started.html] -* Poetry: [https://python-poetry.org](https://python-poetry.org/docs/#installation) -* Graphviz: - * macOS: `$ brew install graphviz` - * Linux: [https://graphviz.org/download](https://graphviz.org/download/) - * Windows: [https://graphviz.org/download](https://graphviz.org/download/) - -To confirm these system dependencies are configured correctly: - -```text -$ make bootstrap -$ make doctor -``` - -### Installation - -Install project dependencies into a virtual environment: - -```text -$ make install -``` - -## Development Tasks - -### Manual - -Run the tests: - -```text -$ make test -``` - -Run static analysis: - -```text -$ make check -``` - -Build the documentation: - -```text -$ make docs -``` - -### Automatic - -Keep all of the above tasks running on change: - -```text -$ make dev -``` - -> In order to have OS X notifications, `brew install terminal-notifier`. - -### Continuous Integration - -The CI server will report overall build status: - -```text -$ make all -``` - -## Demo Tasks - -Run the program: - -```text -$ make run -``` - -Launch an IPython session: - -```text -$ make shell -``` - -## Release Tasks - -Release to PyPI: - -```text -$ make upload -``` diff --git a/docs/about/contributing.md b/docs/about/contributing.md new file mode 120000 index 0000000..f939e75 --- /dev/null +++ b/docs/about/contributing.md @@ -0,0 +1 @@ +../../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/about/license.md b/docs/about/license.md deleted file mode 100644 index aac2159..0000000 --- a/docs/about/license.md +++ /dev/null @@ -1,22 +0,0 @@ - -MIT License - -Copyright © 2022, Allan Almazan - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/about/license.md b/docs/about/license.md new file mode 120000 index 0000000..f0608a6 --- /dev/null +++ b/docs/about/license.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..532d538 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +mkdocs==1.3.1 ; python_full_version >= "3.7.2" and python_version < "3.12" +pygments==2.16.1 ; python_full_version >= "3.7.2" and python_version < "3.12" +jinja2==3.1.2 ; python_full_version >= "3.7.2" and python_version < "3.12" diff --git a/examples/decrypt_payload.py b/examples/decrypt_payload.py new file mode 100644 index 0000000..e95eedd --- /dev/null +++ b/examples/decrypt_payload.py @@ -0,0 +1,38 @@ +import os +from python_anvil_encryption import encryption + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) + + +def main(): + # Read your public and private keys + public_key = None + private_key = None + with open(os.path.join(CURRENT_PATH, "keys/public.pub"), "rb") as pub_file: + public_key = pub_file.read() + + with open(os.path.join(CURRENT_PATH, "./keys/private.pem"), "rb") as priv_file: + private_key = priv_file.read() + + # RSA + message = b"Super secret message" + encrypted_message = encryption.encrypt_rsa(public_key, message) + decrypted_message = encryption.decrypt_rsa(private_key, encrypted_message) + assert decrypted_message == message + print(f"Are equal? {decrypted_message == message}") + + # AES + aes_key = encryption.generate_aes_key() + aes_encrypted_message = encryption.encrypt_aes(aes_key, message) + # The aes key in the first parameter is required to be in a hex + # byte string format. + decrypted_message = encryption.decrypt_aes( + aes_key.hex().encode(), aes_encrypted_message + ) + assert decrypted_message == message + print(f"Are equal? {decrypted_message == message}") + + +if __name__ == "__main__": + main() + print(CURRENT_PATH) diff --git a/examples/keys/private.pem b/examples/keys/private.pem new file mode 100644 index 0000000..f89f69e --- /dev/null +++ b/examples/keys/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChxcVVacwOBjGL +vNlDCZyeD4eaS8Rkhrwb5HcFonjgc7qVtrNq/VnmbALsc8QZ3pLFO8LWia3zGY5c +mirCbFl+RpBX1lmdFSaBQZhH0obVDNgqwG1R2Pho1xEyyUXGggQoY7AqvfliGTmL +8LerdmUBF+o2fSgpRXCiUWZ+nUyLni8NH5nI7mX25+SPRFwvbLkK17frTNnUFGaM +Ah05XOHVDQPFOKeWkQusIDCjdKZbLq+1Rlf/5/F4balGhCkLsuiV23rnNJUEWGKr +686VkcRBuMe8PQzdePs6lUJd9I8qHPeIDUj8vEEyA2jHBLu+f1GINvzfDVEATV+2 +KAc+XK8TAgMBAAECggEBAJ9ut023h/imV/wc8Y5lEaqhKYaCd5qPQyLGJhsyhajH +xbK5LdgaupgiCTuZ2EGantGtVRd09y+oFyricZPNjuGpj6ZRxV3Ps3Qd/oOCU4nz +L7Pqk5Lfn+pLU1LXFGJQTuKzZLKrKfVpYNKvOYTNmTvbouNys7sBCcGfMcFFK2RI +vA5QGjH75KTzYkgeRuEOkMDSn7XNZup8D6HOD8rqMEqmqf0T7LK/+Ui+Lv+0SFHW +bBsHE8WGpvf02szLv8yr5PlVjj/2krrjuxj7TxyDf8bFBcSR+YFCD2IV9DqHfVAL +v5xRETxPXrIuUdRE12ywXX9Rs1gDBtbi8IUAdJs0wrkCgYEA5+hSXMxJ90VaAPxa +yE7oxwjssqOu/XODzae3eVh/RQIw0iVcu9eQmLByAZRC0yRtmL38UDOxPglquQR8 +MAQv9G5yhDY/3tK1vXWpaibBiDzv3OAv4Sh6+rKgdfP4RaWozmGWGT+4REPN1Iwh +hrSQGpa1Loo+ysB2WF+LtcRFepcCgYEAspQuF8PdT3g0t3lGLW0x57ZydcRN/zmW +b17UPUplAXdRm1IcZn5lytb25UtvYus69+DWzLwfwhGKWGagzcojOWJHMzVNd9R5 +kEEEyXKq4Yzadzy/H+wqiaL+euoELuJlnbCBJQYAX417wM81NK0P3o3Z9cT8IBYA +e0aL6zQY6uUCgYEAycw7f0y1QFo0y47MYGlp3WhCr+AHMb0HADnlSc5cUOxrFzIz +07DsvjErw3wD+j9ErxkDKbcRkG4ZVA43EoFSSVyigbHDOl4Yj1iHpzs6RRbhkk3y +2/ahrO0q+/jkHZbdoVkBh88OxThLL50Qv2r3ymtCFdFueneng5cAt5am7tkCgYAH +LWF03xBkA0bLIPqrFLpuv6x2Az/HRD9BlqQdGoJYvrzu6yvtCqN4tY8SxCdj6GRk +VgsMC3uTRzUyPF22J6umwMZvznKgoE+KdrKEjjFEA288X5qm480J3x0vP3yPjdXb +sp5eKJGXRY51v146EcKThUv/Tr17ZKb8TWeOOnfN6QKBgGBJH2XywacG1VPS6Yg2 +T4JoRWiwsaH/8En/dDUO6/YOOPasA28i4VW1xtNZPqk2Kd4Fg+zWZE1pt5w4MKyb +6kSISSo9NOC3LN3WyEqBa3XcRYFb2cggK0EvaJQ+a8bzhcSSzrGvbKFgk9O1QGud +ZjGQrTXebogFV+uB2sUsL5oR +-----END PRIVATE KEY----- diff --git a/examples/keys/public.pub b/examples/keys/public.pub new file mode 100644 index 0000000..9cbb0eb --- /dev/null +++ b/examples/keys/public.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAocXFVWnMDgYxi7zZQwmc +ng+HmkvEZIa8G+R3BaJ44HO6lbazav1Z5mwC7HPEGd6SxTvC1omt8xmOXJoqwmxZ +fkaQV9ZZnRUmgUGYR9KG1QzYKsBtUdj4aNcRMslFxoIEKGOwKr35Yhk5i/C3q3Zl +ARfqNn0oKUVwolFmfp1Mi54vDR+ZyO5l9ufkj0RcL2y5Cte360zZ1BRmjAIdOVzh +1Q0DxTinlpELrCAwo3SmWy6vtUZX/+fxeG2pRoQpC7Loldt65zSVBFhiq+vOlZHE +QbjHvD0M3Xj7OpVCXfSPKhz3iA1I/LxBMgNoxwS7vn9RiDb83w1RAE1ftigHPlyv +EwIDAQAB +-----END PUBLIC KEY----- diff --git a/poetry.lock b/poetry.lock index 9df5566..5b60392 100644 --- a/poetry.lock +++ b/poetry.lock @@ -100,6 +100,93 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + [[package]] name = "charset-normalizer" version = "3.2.0" @@ -303,6 +390,51 @@ docopt = ">=0.6" minilog = ">=2.0,<3.0" requests = ">=2.28,<3.0" +[[package]] +name = "cryptography" +version = "41.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "decorator" version = "5.1.1" @@ -328,6 +460,17 @@ files = [ [package.extras] graph = ["objgraph (>=1.7.2)"] +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + [[package]] name = "docopt" version = "0.6.2" @@ -352,6 +495,21 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + [[package]] name = "freezegun" version = "1.2.2" @@ -383,6 +541,20 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.4" @@ -790,6 +962,20 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "nose" version = "1.3.7" @@ -911,6 +1097,25 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "prompt-toolkit" version = "3.0.39" @@ -936,6 +1141,17 @@ files = [ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydocstyle" version = "6.3.0" @@ -1393,6 +1609,17 @@ files = [ {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, ] +[[package]] +name = "types-setuptools" +version = "68.1.0.0" +description = "Typing stubs for setuptools" +optional = false +python-versions = "*" +files = [ + {file = "types-setuptools-68.1.0.0.tar.gz", hash = "sha256:2bc9b0c0818f77bdcec619970e452b320a423bb3ac074f5f8bc9300ac281c4ae"}, + {file = "types_setuptools-68.1.0.0-py3-none-any.whl", hash = "sha256:0c1618fb14850cb482adbec602bbb519c43f24942d66d66196bc7528320f33b1"}, +] + [[package]] name = "typing-extensions" version = "4.7.1" @@ -1421,6 +1648,27 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17. socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "virtualenv" +version = "20.21.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049"}, + {file = "virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} +platformdirs = ">=2.4,<4" + +[package.extras] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + [[package]] name = "watchdog" version = "3.0.0" @@ -1573,4 +1821,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7.2,<3.12" -content-hash = "6bf4290cc28ce65edeb97a0369157867be6eea9a9ff0fa50116a86e6c76ca2eb" +content-hash = "c62466f8a91e838be0f38105fa6322c8b849b048c1c2eac294f04a315adbb207" diff --git a/pyproject.toml b/pyproject.toml index 6bbb8a6..ac37301 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,9 @@ python = ">=3.7.2,<3.12" # TODO: Remove these and add your library's requirements click = "*" minilog = "*" +cryptography = "^41.0.3" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] # Formatters black = "^22.1" @@ -73,6 +74,9 @@ sniffer = "*" MacFSEvents = { version = "*", platform = "darwin" } pync = { version = "*", platform = "darwin" } ipython = "^7.12.0" +pre-commit = "^2.21.0" + +types-setuptools = "^68.1.0.0" [tool.poetry.scripts] diff --git a/python_anvil_encryption/__init__.py b/python_anvil_encryption/__init__.py index e86c09f..a7f8500 100644 --- a/python_anvil_encryption/__init__.py +++ b/python_anvil_encryption/__init__.py @@ -1,9 +1,8 @@ -from importlib.metadata import PackageNotFoundError, version +from pkg_resources import DistributionNotFound, get_distribution + +from python_anvil_encryption import cli, encryption try: - __version__ = version("python_anvil_encryption") -except PackageNotFoundError: + __version__ = get_distribution("python_anvil").version +except DistributionNotFound: __version__ = "(local)" - -del PackageNotFoundError -del version diff --git a/python_anvil_encryption/cli.py b/python_anvil_encryption/cli.py index 292a5ce..5943191 100644 --- a/python_anvil_encryption/cli.py +++ b/python_anvil_encryption/cli.py @@ -3,19 +3,12 @@ import click import log -from . import utils - @click.command() @click.argument("feet") -def main(feet: str): +def main(): log.init() - meters = utils.feet_to_meters(feet) - - if meters is not None: - click.echo(meters) - if __name__ == "__main__": # pragma: no cover main() # pylint: disable=no-value-for-parameter diff --git a/python_anvil_encryption/encryption.py b/python_anvil_encryption/encryption.py new file mode 100644 index 0000000..ecc1042 --- /dev/null +++ b/python_anvil_encryption/encryption.py @@ -0,0 +1,149 @@ +import base64 +import os +from typing import AnyStr + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import padding as sym_padding +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +IV_LENGTH = 16 +AES_KEY_LENGTH = 32 +PADDING_SIZE = 128 +DEFAULT_ENCODING = "utf-8" + + +def generate_aes_key(): + return os.urandom(AES_KEY_LENGTH) + + +def encrypt_rsa(raw_public_key: bytes, message: bytes, auto_padding=True) -> bytes: + """ + Encrypt with RSA. + + RSA has an upper limit on how much data it can encrypt. So we create an AES + key, encrypt the AES key with RSA, then encrypt the actual message with AES. + + :param raw_public_key: + :type raw_public_key: bytes + :param message: + :type message: bytes + :param auto_padding: + :type auto_padding: bool + :return: Returns a `string` like 'abcdef:abcdef:abcdef' + which is '' + :rtype: bytes + """ + public_key = serialization.load_pem_public_key( + raw_public_key + ) # type: rsa.RSAPublicKey # type: ignore + aes_key = os.urandom(AES_KEY_LENGTH) + encrypted_aes_key = public_key.encrypt( + aes_key.hex().encode(), + padding=padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None, + ), + ) + + b64_aes_key = base64.b64encode(encrypted_aes_key) + enc_message = encrypt_aes(aes_key, message, auto_padding) + + return b":".join([b64_aes_key, enc_message]) + + +def decrypt_rsa(_raw_private_key: AnyStr, _message: AnyStr): + """ + Decrypt with RSA. + + :param raw_private_key: + :type raw_private_key: AnyStr + :param message: + :type message: AnyStr + :return: + :rtype: + """ + if isinstance(_message, str): + message = bytes(_message, DEFAULT_ENCODING) + else: + message = _message + + if isinstance(_raw_private_key, str): + raw_private_key = bytes(_raw_private_key, DEFAULT_ENCODING) + else: + raw_private_key = _raw_private_key + + private_key = serialization.load_pem_private_key( + raw_private_key, password=None + ) # type: rsa.RSAPrivateKey # type: ignore + index = message.index(b":") + enc_aes_key = message[:index] + encrypted_message = message[index + 1 :] + + aes_key = private_key.decrypt( + base64.b64decode(enc_aes_key), + padding=padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None, + ), + ) + + return decrypt_aes(aes_key, encrypted_message) + + +def encrypt_aes(aes_key: bytes, message: bytes, auto_padding=True) -> bytes: + """ + Encrypt with AES. + + :param aes_key: + :type aes_key: + :param message: + :type message: + :param auto_padding: Whether to pad the encrypted message automatically if + it does not meet the required block size. + If `auto_padding=False` this may throw an error if the message does + not meet block size requirements. + :type auto_padding: bool + :return: + :rtype: + """ + iv = os.urandom(IV_LENGTH) + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv)) + + if auto_padding: + padder = sym_padding.PKCS7(PADDING_SIZE).padder() + message = padder.update(message) + padder.finalize() + + encryptor = cipher.encryptor() + encrypted_data = encryptor.update(message) + encryptor.finalize() + + return b":".join([iv.hex().encode(), encrypted_data.hex().encode()]) + + +def decrypt_aes(aes_key: bytes, encrypted_message: bytes): + """ + Decrypt with AES. + + :param aes_key: + :type aes_key: + :param encrypted_message: + :type encrypted_message: + :return: + :rtype: + """ + iv, ciphertext = encrypted_message.split(b":") + + _aes_key = bytes.fromhex(aes_key.decode()) + _iv = bytes.fromhex(iv.decode()) + + cipher = Cipher(algorithms.AES(_aes_key), modes.CBC(_iv)) + decryptor = cipher.decryptor() + decrypted_data = ( + decryptor.update(bytes.fromhex(ciphertext.decode())) + decryptor.finalize() + ) + + unpadder = sym_padding.PKCS7(PADDING_SIZE).unpadder() + return unpadder.update(decrypted_data) + unpadder.finalize() diff --git a/python_anvil_encryption/py.typed b/python_anvil_encryption/py.typed new file mode 100644 index 0000000..1242d43 --- /dev/null +++ b/python_anvil_encryption/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. diff --git a/python_anvil_encryption/tests/conftest.py b/python_anvil_encryption/tests/conftest.py index d51e280..2c376a1 100644 --- a/python_anvil_encryption/tests/conftest.py +++ b/python_anvil_encryption/tests/conftest.py @@ -1,6 +1,11 @@ """Unit tests configuration file.""" +import json +import os import log +import pytest + +FILE_DIR = os.path.dirname(os.path.realpath(__file__)) def pytest_configure(config): @@ -9,3 +14,30 @@ def pytest_configure(config): terminal = config.pluginmanager.getplugin("terminal") terminal.TerminalReporter.showfspath = False + + +def _get_data_file(file_path, is_json=False): + data = None + with open(file_path, "rb") as json_file: + if is_json: + data = json.loads(json_file.read()) + else: + data = json_file.read() + return data + + +@pytest.fixture +def forge_complete_payload(): + return _get_data_file( + os.path.join(FILE_DIR, "data", "aa_forge_complete_payload.json"), is_json=True + ) + + +@pytest.fixture +def private_key(): + return _get_data_file(os.path.join(FILE_DIR, "data", "aa.pem")) + + +@pytest.fixture +def public_key(): + return _get_data_file(os.path.join(FILE_DIR, "data", "aa.pub")) diff --git a/python_anvil_encryption/tests/data/aa.pem b/python_anvil_encryption/tests/data/aa.pem new file mode 100644 index 0000000..1fb281f --- /dev/null +++ b/python_anvil_encryption/tests/data/aa.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCAKW7Oi0xnjxD0 +h8+k4kVTRUO71vR4JIYMk5caohssGkqGcOFPZfubW7SKTr1uG/OxDahcnEW5rWlz +9USVVP9eb0X43JYLRuz6Z5uo5aftbLBjvpiABNwF/Q4N5Y1UQoqyUXxkRupcblHP +CJVYSBp2yLFToZP4vJSkouGbgCU7nXM4KkNqtE3utSAW2TaI19b1vXa8yJq21Eq0 +IAxDTGMd8W6U5YgEZ7ZVga1gPllip/VvU3ks8uW1GoD0hX2JlQ1CFMEJlhkrMI7V +dakRPm25WKSHU6OLDR57WIy1o4jsbvppQOLQXJmzaWPFm2Lq6J6QfamATaIHBzDx +06HBDGBxAgMBAAECggEAFyvmjc9fpAWY0i1P3IbQc+q9AVQu5R3tYo64ricK9KFY +QhBcnCdbI06kSLFNzH+fQq6udvgGHzj8LOv2V22DskY6B3GSaz2KkQd0fEfxYZra +FV6bYeH/CdIj3V1YfRh+zRiwfczYiJelkOIDZm+64SkN7onzH/Cwi0ertJqpoKpl +pdpQxmHad4PHdPssKNJadbaOCL9cyE8y8iE1A3hWIVt78mlUxNMBoDt90qcCUddo +CP2dIolMKBjrYSzix5ZzomblkDlR3YNWikE4pwjaGI9KX4ldsciWrTdBnDW6GlPY +vm9eseaWoKfRr0AZVPHtpXcmOMHRABREzKVRu8RIFQKBgQDyRWZKxnHqvDwMDHQu +OeCjkhxL/MjF+zOczf8A/Yw+UMzEkZWuWm23HvOERxaIKoNhEGqo2WSJvrjCDkOy +UjrLFfprT/rYu1ULUpRBXWW3nWWho9bshXqAEF7x6QLFf8XKtSBicDzJRzndxC81 +bKtjS8xM0/20SpM2h45Kmih8ywKBgQCHbKmvjsqyrLtBcDTUBiUgl1oicnMgYTtd +893e0OFnHUN89+X7Gv+KLC6qFARipapiG8SfUpMTZ1UIEv7iLBzlGamDZVO8j/ri +IRQzbxwHxJdR/S5vLmaDPiVLTcFFvu9AHhOOoQqxx/n6eklnAVV7O5n4Z8ea53EK +4VAc/SYMMwKBgBfGBZ6q0HznUcEg67mphkimys4OFSmQV+U4NaDEQKHQzfcwDg8Y +4pFIoT7GtwhPm6rHsJ+3DW2S4JByU+RIu2o4UuV66LOh1luRE+lCH/wfntx4tltp +UbcFZDu60MHOovscQPxH5T8fFSeU28kTS9InmvmjB89MThvYu3bIn9Z3AoGAN7dC +HMNfq6EbLXIPmgepML50S+XQbcrzFNVnO+uuQLEuefS3vta9ucgxrrGrmDZWYiTQ +gUgE0J/bwQWnrb++Z5dmQXPnvpVx/6TiXcLhwmRkaUwD1aQ+ctKXn2KvGJXUcwms +vCqPbtM6io3TMzi+RYQg9a0k1xtPS82lGzgfdjcCgYEA6q4oYN/c/0guOpNCV1tk +agyeLPMK3pHAzxRCwrFoNBLXNbqiIxyLQUsZvNZyEAhpsuOQHN/uY5m9jSCGNoRo +x1iI4hme6E4Y4NHBqThgcGMIhF/fpiGIGQHt/VqqVx4HyfZ2openRVSlAQA8fBq2 +fcs1DqIl2gzRelnA0U93TeY= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/python_anvil_encryption/tests/data/aa.pub b/python_anvil_encryption/tests/data/aa.pub new file mode 100644 index 0000000..b7e72dd --- /dev/null +++ b/python_anvil_encryption/tests/data/aa.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgCluzotMZ48Q9IfPpOJF +U0VDu9b0eCSGDJOXGqIbLBpKhnDhT2X7m1u0ik69bhvzsQ2oXJxFua1pc/VElVT/ +Xm9F+NyWC0bs+mebqOWn7WywY76YgATcBf0ODeWNVEKKslF8ZEbqXG5RzwiVWEga +dsixU6GT+LyUpKLhm4AlO51zOCpDarRN7rUgFtk2iNfW9b12vMiattRKtCAMQ0xj +HfFulOWIBGe2VYGtYD5ZYqf1b1N5LPLltRqA9IV9iZUNQhTBCZYZKzCO1XWpET5t +uVikh1Ojiw0ee1iMtaOI7G76aUDi0FyZs2ljxZti6uiekH2pgE2iBwcw8dOhwQxg +cQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/python_anvil_encryption/tests/data/aa_forge_complete_payload.json b/python_anvil_encryption/tests/data/aa_forge_complete_payload.json new file mode 100644 index 0000000..809396c --- /dev/null +++ b/python_anvil_encryption/tests/data/aa_forge_complete_payload.json @@ -0,0 +1,5 @@ +{ + "action": "forgeComplete", + "data": "KT7PFc2eiDfODBVTj5SGCX1PMHW7iHvT4sxampMPD8O6LXGMsSzYIss8kuEsx14VfUTnyLhFfWdeHUaUofa2P6P/XtRBEo5hTt7W6medIcAyQEfwV2XFGrRqS+mqU45TKEUhj7I5lDCXRoiLv5ZwyUJVnvg/hvPq4ZP6kFGANoalzcmFfTtgybvrC4kPspp8oBtaL3rtxI5RdjmE9XN3+HhYNPr/3zejiC5e0RNXIL2r5tHHp8JNBBIsVzK8Vhqinza9x1rjzr2oDK2VtWA16dOCoyGsHQ3Pi3v8TRVrsnLECj7VWxJtP58IV/Qpq5ApnjuaLXABCBzKYZwOYFhNHQ==:05b7c9301fbb1126f3004c15e74e202d:859fd68808d8d95aab637d6f381e8b991697ed28c98daa12f35d412bef7126ab04dccf843fe0489a0c4ef77ed75f2fcd0172bc2590b6a6296e8803a38221b08ed54957d80747483d1886144170ec87c48c815f25be0c00ef2b27137f706a2837f6dc1f616507f681e2a955326daf18ad2a1b155abf1f25ae101d7aaaa18640a0961dfc11028f8c9915bc8395191e73d78be7b3bf7cdf367d5a3eebce0f2e4834a9ad89c0d5b5e2310534b17838b7c175a90dd03eed1a599bece0b8bee4c719d82b6d0d51b7cb7987a4d9bf0309a4e7052771b5b0c440f2ce5e19b8aa4bbc1d2ea991f4cd1685eb12e96b80a9a157c84680e76336db70aed71b6e4417d7d3d0d36cf3934a6e33fdefb8ca464bffe37de5b03768f9c7f198c09485641e102c2ffc10b8f4a133f8ea95b37f93819a0bd4aede723d02e8bb2a0ceb388cadf15881985e597923c33f3e790b18f9512c1d2e17367d21fb2f9cafde4f35cbc0339e0d0663990260798fb47d5b14bf992627aa624e319579003511320ae03163ffc9400c74f5fee057b427ed034fd399353f314cc2885311ea10a127c0e806e4dcda892d559c77e35830652f1f0796688bb3d228dd24f0a3b4c74da8573f9bbc4461d7f470c069c9938314e5e84a87cdbedfaac54e56fc186a849e1e36982da7261c7acbb83ef719014965f96949151fe7a09d9077be5c966722e87aeeb35ca379c0fa145d3efb44939926fe2436f898d6281c24f594852b2550e5febcb475f3a3985ae0b1a49eed67fb5531d46757b2880a07f7a54c33f60a48d195460684c57d014986e45bd418662b83b454355a75b1175dcafa1253c1ade0a29b9b5ba09b680c155ed9c4dc8c067bcbff5cd676e9f7c59d493b1f9cb0523312f3619de570d87e34f5bccec727d11bd01bca1c36f6c227b79fe0730ba9b046bd24f37740c954d3419a66ae2da8f5e5c7c294e999c3e2355b0c147cbceff1ebbfe623b6e50239d1c2c20a27e5a7369f27c4c8393f6488fe272be575ad303f9f7ef936cfa10b40fe3d23d8ab9a81c9505da3925f87ec564cddf87e09326d6d4ae6dc6ce5914011e149af02c7920c659fdb432283786ed88ecbb59063b0e70b254a188ee7d4e00a71a3fba9b708f8dfb68a63361aa14aa1ae3ae7c6a6c3197023eb5ca6f496000095c56dc72673db362197ef31da70d7ccce596d98b870ad9ebee65b85d56ba790c6e9eb3cc5631dfc2275369482fdeca21faccc00a605542934310e267426b0841a10253b8d0a97ce2283abc3cbcc133fff4c05f38183934fa3abc686dc998252f69209ed7b83e244c791714b2aac6dc29dcfe0923b888c13ce2e092895eae7b5629f0fdfaec4feab1c19bdd67f1c461d8667a38f8c1717d16b6af0e09374b95ca42329f4e3d4b5fe21682fdf2c4812c06ff81c63c5ecf04d385693bcf5137af77bc2124ffe86542ca3aab883bb846029c14b3be6826c89697f661ec3498dd08da526f56b06a4b3d376417edc8c0fb80cc2b49fca4eee41a0d14495ffabc003b0806b591f0ef12ba79c654953ea8d8996e37242045165e0ec6112b7a74578b45d4c90e3298b09e0b093a93139a400ae4f166d40426a8586af31ed6337e86aab1042ba9f2268382d1ec20da31a02236245510893eb3d3af426af3fccd7a9ededacf093e451311b52016088f956770ff01ec327cf230c0cb1be45a6ab082bedbe2f3c14775fea83eff2e90ccd2adb5f8ed4ddbb731b6d961e037ab3d17c46c8e920f0d365d395d88eae5b838c8ecdde4c38807059218fa1ff395f9ec3a4ae45e236d104b9fda161c03a881c9e994849cd256194b63ae6eb9ddf00b4e1a7ddfec8b0b1828bc5b895fdc483dfb9e4d1998d68826017da0c8e80aa571eff0e40588ca2e85e4d6a0c31c545434ef155795de16eace9676c571717f7ba608c345b56d21a84ffd1a182725300ded1d0429f2952cf56f9b64e54973749a4af6f87e538339561cbb5ba7ad37a0e30bbd2c94900da1663e4dab7035bae025cd8afad27fbb5c8ed8d4dac25fead691af8e188d6ade8b1dabf995fb92a39030ad622d86b54f016cc71e238c8fcfd5205e19d5d39386a724714ff5d32d204d66fb40cb5a25248d502edddd32f9e72eff68b26653935361be84589fb997c1e47968fb8c8a141994da2cd75252752e64415bb4c0d7677e472c597bdf61f91284decd1a9cfbe6c31fb770d714402d7c49b9fc36e51930e6c190494acdf3e5c43475e697e5c1ab6e1058770eddb96133709563e225ea77f433b0560653c7ce3111c603dff18a4e9cc735a570958a49650d850bdaa0a05f9fadb353779271756047b6db6c1d2c3d7e6bba88fd9d4ff662bc192c8d39007a200729179d6ce96c4c1adca59e83ecbfd55055de0b86ae8e3501a163daa560784a0dc890a2174476d2eb1a363cb8a1c12a69ff6153d89c2866662cfd2ac55128053602d08d3e8a45a177fc6c6e1d9f1a06b6d8a5e6a4ff8a0cba90e96c566450fd594e88a9a6bdaa3dc951758cb336a0ccacd19586f1bf43bf90aa52cd1e679947d050e8cf913223b803a525bf44edf0a3fc32d4e4f2b2e01cb22f8004880972467b10c853a7047033fa87d80d49d94555cb38628b9281958888844968009a5f1706f333e67466cfda31f5ceff5768bbffe708957dabec0879b39b0f90f711986ea7388e1b2de87c00cdad3eb04fdaef08f079fdd3a5dce7b095f0c2f4751fb7116aca7d2286623f652da1026cac0a42f1500f05a92731fc6a5b36fbde27c863ba9eb00983cd5cf09ddb45d150d280b27f601389e7a167c32b8dec3942f41246f02cd220b6c3e6511be7fe37f76ca3f795f94119b733b766e6fbf0f9de05f37e19406532cf73de2d1e6100749ab073ba384f9376f6bd6023da8334da5b534fd2d781115c0303b8a3fec7ba7cd5e9d8c051507fe3c084515be4dcb67427131b93460a0845ffb20ae24bf3e92b9212ab84212a44e6168bb46bb8ac2e162f57c82ed998d49b9d1413c196ce451871e420289efe3001abe93182b81617f15aa297efc51878853ead9474240faa1384657d86d453a87bbf8a092a3ab6b7feba440a63172ff3c11677e6dfe4742093829406b6397b937d3461127532f7d764dc53dbd59b3b841d24ff4e121f5b8ad0dce718d8f8d8757df38f98fa4fe074babb6267bb317ba22891fe943e10a0c417badbc5fa7281fdda0a15eea9fe825b11dc8bee3292ef0e91de5e103f381244c78edd9653a77ec6de5d9dcfb41814687b2b5a87ed6766fa23491a7727784821823976e8d06ae5175c2c904435916ddd27463e2fcbab46f9dc3cf7c980c8abd12e4f45d19a64cfb89d70c323e251ee336760bf8b76be162f8abb0ce377066a714ef12b49fa8483629fec1d1ddf8a9dde077bdbb5f606fd24d9bedadd1eee47d120f1b9a45579865b071b3eaf409b7cbcd7c0a327361edbc590b9bd7819c6c0db310fd578a2fe4de26553a8f1840dfafadbc42c19864f1be74f8d8a610517b8978ae6e1fdedb4028ea3ec44f82984d653387ec479c4ea332e7780520af15faf4eb66cbdaac1eccaed0ff8a0787db7403815b5692a42c7db4e8ce107c37b677f1ea4f0bacdc2f12633f7bf955c3ca45284351de22e2a434fb7ccaa7c3a1c8d88c466e7db49935f4853bd0642201718975db28ffce8c2a47be9ab116390e6d642d76b0bb749e62d4496b20c6ef22935b39eca1042a4833cfd37659678c2e9be651879db3631fac431ead51b3ef512c732fb89cfa30b138b1a67c154f790e44c1c7a8308d259b1fc88ffc92409909044685bfd81f672359a47bf7ab73d49d11ff067bb1adf91d0326101b3426e126d17369b1022a36e1f29da7b50242e9900d738e325d869a4a9c070a64d03b654727829a4c9f7ddf2a745b90b41b955e9a869eed298809bdc2cd4cf9d62c6e9f2dc31956bed3927b57ba3a573556f9ed12cb3a8620b33eafe4f11636aed5c638748734c51a81a39d7359bad8894134d139ecf21bed42ce1ff547c91e6f1603dfb67d4f4b94d6a34eb82f9e2cde91730219ce8ee9b17bb8dc96d3dcedcbef8f1c3766d24a1c81c2ebadc66bd53e15acb2269cb63d1ecc3a3cc017ea3b4c4a23b9bbdb0bb8b9020aa5a210a7336065264f53fd7ac6a441108e7aad5418fdcc605d6e35a61a893a8d78f9e85e5bb366927de13eb24accf67b60eb773fa35cefd120cdc7a1c74acb08044006197e04653db7b2041304e313297e1997a8a08e3867fe52ef8a2f23e3764ceb0f6d9fd83daee05be2d36a4a51dca460d5858d6971ae5b32a27ccd0cdfd0c13e6c937203f133e651819b874e7c313ce44947139a9722195d9dc6b165aaf0776f95b07deb94e27125a256f7d1fe8bae81633014e44e16eb12ee20909885025d55dc0210295dd4d7a84d818adaae93100af5a3b04cf8c1284b215431f09f4f23a2ba7a890e26412cbb40cca9d0847562cdcdf7a91059a19be69e846e9f2b9ff268d8d1cdd0f51308c10cfc6ed0045b7ef4daa45f19b3c7570fc769d742f3a6d25283db1a615f73d1eb167f24fca91924042dfe6411d7b0b63656942e0dbac6e873e419d753882f557bfabb916a36cc79b13244a4f12ab6820827b06f3f4a864f5f21616fee3fa93c93fc9a0000d8ee441c00506f44ce472e294fe45c0b2177c6a51ff17a7c0eeca0e5cc5b881b54de1325f8fdb12dc710e8bac91c9c42ea65d097f23d8ada2b379438e6bb51decdbdeb6140ed0ba08907e9e89a0c7e40c77c9c8ab3a630ffe357b47fd788ac73a537272f304c68b2ffdd76f4074e5c58fc0abfadb6de19f22defb4a0b5f819f040a990acab76dd6b30485d493857aae98a8e2e857e8a668454687a58bdc8333263e346cfbba34a295d9ba4ff46ae49d4da848fc26aa5b9e9cdff08d7aa9f707e8a2668e7fd6d73c1051d2fc128f8c633e2b4f583cfaea09294b71ace06d40a3c4e47fe0c475d95b5d1d7c922e63531ae688f9ad1385b39018a6332fdf3302566c0b993c524fea38ff18a052147b32001bbd02e20ab85b37ad0e9cd7f5996e708264b0d08800bf5a7c8ef2ef175c1c94ff9098106b89ec06d1fcd86651fdfda3c5cf641a8e436b7d059e54c932c41392ac7b7427a619dc407f7a2f2061e7d97fa354f69139f518e46bcf77695f8059bd10ea7a55b7b939d48a15e1110706e7651763834e568e871cddae08e418e203b0c0f379e8b59b3a630f4b029f146a1d07d0f889df717af3ca675bf27f39bca2d03ba314350393f37590090fe3252e1040575829681a6d1ec1dc743f366af2a6ee1bfeee7ab7d21c22f7a7eb9d99e2bc0dc4487c30c0a36d2e2a131d0faa0c69ff554675642156530940e795e12f971d241dbd3105a45fbf531282e1a77bcaf813bc6264cda7a698acd02047c0bef58e82051188e19dde44dbebb087db514a51e81506639b6071853f3a45abae35253bf46ce8d7edc2d13a7ca2da9163002aebceb187b2b5b8a209e42e446d37884fbec54a7c44324167be041b886fcab18ea8c10bc47271101066f140c7d9f92dc0769d97d818b31ec34530e7f87a0fb1a8e1388ba835e80a83f4ccda41a5816313029ea8378aa6a2c964c6c5b8c69f4405798647e965340726ad4dcbca2d426821b1e9783c049398a4c64d720d8df8c0537c0afeb5633de8c5830142d9000109f76ea4aca7e14081fea802edd64d8b04a8d814d81b85e097227ca03451b95636f68b835effe1f7ca5c585046609f8798d1386717f010686265f0597b82626b4fc362a87eae1bfe49942b05b228077ed0a2c23176897a80ad390b90201d8d90afbb34d42200c88925fbc5141205cfc6af1b73e663988192b310c5f00e0445ae6ef59f776cfe31d38aefe5215b5f024d5163fb2685790146d0d3f9b4d22fdc3ccb8865a3683a020e79b008d33a2ed822bf7881808f89ca56e0adaa139bfdb1bd4b6b2100935db3d7838d76d1856b2863903b4584eef3bf47749b3be1a2bb4b8e4c91a7437cc4e11b646ed1fdddffae55cf6fd348a75dbec362416bde9163eeefdc3a9776af02317c2e1f3ae9ebd4d5dd416a07e61e3c997d3ca9957c4b038a0e76ccde2d203fcf8a9f945ca6a1b44fc5aae44d8abe4e4673c235cb5c2185ca5876d175d5cbeaab51a01da2f4e5b6430daf4b2ca284c6faf621e4f7f959db9587bf46dc58ac62ec7bb6cc499ff10bd941c86316eb1d40cd17f3279147687a30c403226cc16037fc8beec137534d7898ce65cb9bec2ccae965544969c22e62849ec4dcdf1f6968e4b036bbeff0bf2b42831d174eddb31039b706ce1a2ddd85e750cd916703b1397e60430d8b7a8bcc1426ae5503e0a9cf615083362b3ff76e0ae29c93962e59d343526956ebc18e209f35936dc392140ae7c6b45ac55b77b743d6745e5dc8ab64a9a7727118e963f0fc1acecadb30ae9e82eb8056c76d6b14626ec095d7a67b454ab64b41c58ec838085bda1ef41fdbe47e9a836cf3f709a1e75256e26961275a255b8a67a7b98066cccefa7a904d7040bb93292ff8a8934f069359bf7e30b04fe57cf958a8f8ddf56a52703329fa4b4c5d199c4d49a6b199645ef3806d28fb49c4bcede619a5d6ac8736bee491bb2aed856b037a0fe9c534c5aab79efca47094e3436245d211b12b0461990a6a32233bbc803ef4d3fb0e1ebd83211893822e6da05ac3c06de0e4e918b1fffead3281e1fd0ad36e7905725c64e1437c321745a0a13114038d08016212ce3dcd7dcdde37c6df0d602b26dd23a5cfaa3166a3afee77ba266accb37ebfc9d40ba2faf4d2ed26a9650223d47dc9b06326e41d7b0e9b2de441ad42555c9784f17352014ef6028884100c0bd84f42151c14e60fe4eead6eaeb0c5b54b3016887cd5fb096a4a5a8be8f9e59652b2029fadb19e19e6d02eb9b37ee7cbd269d6351e7539d3a8a489b535143925232df3ad0161cea90e0ec6dc035444de199dd8cb63f0fedf381687fc31c22449767d1b9d5eeba1b0e281b66140908b5ae0fd90e8e18fdb6a44dd1ddc1fa1482f3daa6f25737441aead411009b42ff08e8f836cc835cb72b65e614296de7bb7da0ba2022293cbc1cf059c8efef6cad0ea179d7a8414c26da769c97321bfe96b86e666c69f8941f962da7dfc0cb33880f605895e79eef25b70577485bfa701efd13563e47654b642fd3f655d63d077fc9bd777214e8b258f4a72a5cf3dbd02eb77e636015093bc45b6cad18180b19ebf7c9f7abcdd914fe36f8aeed4252333a144591b375b0cde2312d7a2803af5c0ebfc237ae752a05be17add28e2764879551747f5bcee8f92632fe727a768eb04edda2f97997e99fcbde5a97afee932d9e2119940af4488650bda2e368f9e3dca3ad2265e504244a2a7399f4f6f8452ac7ce37ef6efbbeb9c0ab7d791ab168f29ba4fb0e55b723c6fcdb310e4dcbe07bcfef51aacf9ec50def3c306d4e34d1173c9b6a590a59b3b93f0544da6f8aa4fd3530de63ad4df953871cd65e277668d9957a79b22e0113d76f849b76e338a1f6d11e2d6522a5556908765a861b4db3445c43bea67803f05058823288bf96bacf33e831476c5ef832bd85867f9eeae9434ae42d8546d2e40a0d7e462698363dc2924dfb56671514104aee85770bd1399a3e431a4d6c79fe149352dd23b0cd4df69348b0d00c069f561ec7805f19bec26d83e3baf64be8458f725f3981a8246d326bd94abf2321051511580b19b7a27a3b260e430011274c05ab77fb6854ae5805b078cd417f92005beb9eb7b8282b444579688b49337369f4baea4d2930344df8143343dddc6836c41cb20eb7f17c2a5116d969ef4f3fed286e1e37754c02cc51d5e61be279996e41c25973de410d55fb6fde2a9e8ea20bad29a770b497423c11283e9fb34dd0a1f059e1940a410ef422f4881dc22a7aa1919804dc18dced6ceefdf2441985db8e062219f73afe093e70a99cd3f3a5c73875043476892fe3b22b9da4ddb8803aab9b361ccb2eb5000e0e829f6fc306d9fe6538fc81603ffc26dfd8efc77824c1228057320a3eb48d9012c6cc496a4a9a3e9a5036aff4b175e37fedbf600d4c8b2719ecb83e52766bd5da684cb1601fe67e1fbfcf08c3d35356ea22c71a8febe799e0d5646d8964376c640b8c06c9da2a7b9fffaa6f88c25fecbeabe85a20f2b42f5e5f3dea087ecf1e2c0ac360c963fa516e3d3d8dfac09d83889fe747a01d59795b23fa95cfffe2bb5179455ce47ccf74f3de10c5c570a2879ddffabe9168c20eaf9c83996e5f11f1230051955249783d9ed0712feb8071df57108a2786947243bd4d1fdbc0b0db9016be54e214781cec6259aa332847d180fb653e9e60dea82c7cc655eb22d1fe5fbfdbf309b6dafd9d3a0985f63f36ab38ced6813a27298cad6ced7d4916b5a3bd96a3280cb68c31a791b2124cbdda51c2b5bc2b1036b7190661bc1c84a2c354fe1d717aeb196089b9d5285cf57f0988b158d229b798f019037d208ddf8549c18e09ff5eabe06e644ba9759950870e604f20c2a3a91a0357849fd4743991e97aaacf94ed186d4b4c387e9e9f1bb4469bc5bd8a0050dbbacbe9053d057d548d5cef3a3bf07840f978a1d3f0c99ec81f9b5129a0a123f25e54b975003f2b987a89e3ae99f0fc524164993e4045ee0f599663c66e36012f3875b6f2fc48a06393815d40dad895778b1f0f3755c4ae0ca43511225f65a38d5207b167e3524f15b97a5233d8dfd433454f98dcbdfe74ae8b16b7e2f421a13825492bb09f377647dd4905c3ff9f07d8674f12ae9d52199e13b34bc0f39ec035e1a9aa0faea13020bb5ba50e77527ae716d3c51ed88a152e8a7c561d8a39db8385c55803b068ad748b3038b60451bb13ece745e65ced5420561b3cce9aeff313ba13cd6c7068685eaf7fb18bb07436b58ddca3b7209e581c48c4b6658cc83e2cd4e849882b00813ec90791de70dd7b90b442683d7696f517f78d97148ad6ebc17418b7b08206976183b2367a39e59688140e4fb077b9cd90c11c409a387ab2d9bc92ff1dbeb991f722f480f099ae12466f8083a20ccea970b06f8cc805f3961553228e946117b524d99ee46b4a4b9e77086623115edd487f270eff99327574cc84d26753bd868332d8dad9e14f3b0597028a880909d04c127a93fa553f1a159d8fe078e7484357129fc4f251a4f966af12547cd94d1891f2cf443c80a55f2ac366bbc5b85b86723f3aa79f6f7aee377ea5b02d989d38c1b336aa26b8d471e36fa036314d10aac2d00eb3eb567426dc1f9b4b4559fbcbb100c58c48d7aa8074c67204f98246b01f4fb57f49d648ff231257c1b291de305076329268703fd157ccffa9aff37444a02a7ab8f1d1c7c88d19d215f7bcd8369451d89d267430142faf90798e009c3f4347fc97af3320fd4dc8af83fbe6cb3b78d666ce9c35a699ed49d942f1e639f9e0470cfdd6b905a281918396c50441efbff7364a38c5c8a60dc7d4ea343b4b6b776136381898b9f30adaf723d2dc9fea09aa6b1bcf9690cebacf1561efc8886691ab33fe7e81af19b68e922a462ea673adad2e1ffa4c17d8cb13efaa7cb3cd065357795b58d532ca18f6307f500acba97930203c724fce73a1013f9176707492455b06fec64c394d5ac6e9b28aa39a5b01fea9acc613297ff33f0a98a39e8fd1d13c0af17f82f5cc31947b0f6ba07fd30714ec545008e8d41957e49f5a11dfd48cfaad39b0d1450e3976e4396291cb9f18c09c6005090ca684f7b5f11d07dbce73d87ac0a212395b78e18d4aa438f86b90bca640fa07fd7502236b6964545891d417689b5e2491bffc0f486496498dfa93499c3c13ad5d04f7ce95cdfce665e8e13d78f788f344a953f0052c187432005ff8cb9cc7aa1bff0e2590568b501e1b655ff35b2ef52782a57b6ddac861753ef1312088673b8242c53ca018634c6ed3cc742ef2286ee3aa533555811b4d3daaf7a4cdacb22fa266642a8bb31a457b4e332ddceb25707bdfd9a90bbad8a631b62f1185f342f13301c5dc98e756ecbdfe69baf07c60710486bdc8fd07695eb45673b7fa3654c705fa09d7efda12c1c808b7fed95e5c608f3769456a65774dc36416bd8b43618e4e20024ba0e5499ac6607ba34d63dd4f0129e173f33ca224de473a176b767ed71e807919641798669024a14651cba2a2f61aaa5894f46695138bd7858e08fc7baa2bad0fead18c669af4150ea70db7ec8a0560995b643c8112988c162b688345185c71242f6d17cf85db86d54d35223838b16c555e45f29058703a49d90b85bdf8f429e87a82a1fcf681996dd3293699b4a130909889185ef1659aaedf55240c03a1158059a782215230a30aeccf7d975bd49c12b0cac20ae5abe5d2e4957b2a9008352cac952abd79b5056307833df10efee6d37b209230eb7c7f7493392167d7654af846c5f22dbb809df694b8c3ea3ed2e07f08edc0b7f894b43273abfd2fb492895aa1bc0475564def6b7467a6400cce71e81b2b6186285386a2d2564f35f97c9badcd08609f8a4d9c63c833afc51e5cc6f3276073e0cc4f2948aa4c4324112cf82a99e1de9b388251a27f91233f65b24ea37a05fb534ec958d76904c9b19983bd620344a1d1c9a0f57122bef1e6d7efe7d353e1b48db34b12d3239ed6bb480973949d2a8ced36340576e9a4d322e53bfe95e329097daa7a9869be072eabcfaf856c24fb05fc79d32d239ed4fadd1b51c4b4f40c29949bf9890724b08ce43f6992dfe9a8d8287f3e107c973deb910ff7a30b6ed622c3a3c7178efdfc01a13cd91fc17f14ff565a4f2db0b1f92f2647ebe74a49193b1d78ea6957c9a1dc0c728543754f5db2dc21f551682f2df008fa52fb4c740d6238b9e2ea38b26134caa5704a18b6a00b911957d3bdf2ac1151fcfa1c928bab84cd1cfdcd614af784259f7fb1fbd143aa5b0e0dff9a27669c9b7058419e3b9be57861cfe69e4748913516b33d3b85642a89d8d16e440ec3c8bce6857d1e6861940d0a7e0fb340c0a2aae519420b293b97b4fb78f2bf35aec3ca835c9c9b2e316d245fb25ba34a550e9c4272a49c031d959df35ba4c97e6ef81e0221edf0e2b32e90ab99aa809d4514c909af8a08773fa3b942b4ffd94797083b527846d57da6dd04b9e82ea671987cd0e777355fd4dde4a884435ebf0bca94c9dda2edf32df174b5cd2723fe2403037ddbe458a6302e91727891eb9e13b172abd5491e0647c2c3fa878c0d94392e60fb18ba4559959936ea9e0b3c88dfac4b30e30bb4bd3885f3ed49dd2fae3ef1fc44afbf63a4c4e09858519f3a341d395d8089adcba22c48f79281dae654612b51a1e2c7e47a94c31bf59bc062809978bd051a0969281fa769163af19f4a74776c4396a00d0eceeab2180179ccfc38afe1fd3f3e032cb103d9b2d58b1c54501ef137c4c6c6c157cc8678a989d0817387910558180331221494155a0e8ff58295418eab405c7113b4aab140c246871042e3abfe1f8272d1299245e6d3ad71c74dd6121d897bd5669f911eaba83ba4c21139a9037d0240f41ed2439b5b1de5d324fbe90ad6b36a268f9274609594ba5d6f4a2db9f066b6d44131292ea3313e8652d3f55574a64e966428b7c24705c9b9b6e32c40b31a69d333d7228a77a27590cf457c68f4aedf9d27d1f35c7be025d6f54d5d04acd22c532cafcbe144e2e5f9d64273c64c70006ba14b924dea5055483bc4a98ad3670b184098c1ee402eb345cf5506ba553757070db830297f1b7f1765dd8de5473a41aad4b47e06442afa4e714227b6f3504ce3326b6fa333d02265e7676d9fa625a8e12b42e7ad9596685c964df85a284895ea2b6766c0c35362cef29fee151b0a5a2af016db7eef1241d0ee465a70d43feeca71dc62762d7493e903b724b9fa659327b35faf49fea6c5e656c5e822f7c54405219bf12585105734c77a6c1c360a8a78c3c85968a81ce5ab938dc1faf2255c358290f4e20cf016c3d0d66189de0759647348137874fdac131d6302dc778d03a839", + "token": "nRFfveeqzfFnpSManyxNz53FU74Zi7rC" +} \ No newline at end of file diff --git a/python_anvil_encryption/tests/test_encryption.py b/python_anvil_encryption/tests/test_encryption.py new file mode 100644 index 0000000..823ab9f --- /dev/null +++ b/python_anvil_encryption/tests/test_encryption.py @@ -0,0 +1,83 @@ +"""Tests for encryption module.""" +# pylint: disable=redefined-outer-name,unused-variable,expression-not-assigned +import json + +import pytest + +from python_anvil_encryption import encryption + + +def describe_encryption(): + def describe_generate_aes_key(): + aes_key = encryption.generate_aes_key() + assert aes_key + + def describe_decrypt_rsa(): + def test_decrypt_real_test_data_bytes(forge_complete_payload, private_key): + data = forge_complete_payload.get("data") + + # Convert data into `bytes` since the fixture parses the file's + # JSON, converting everything into a `str`. + res = encryption.decrypt_rsa(private_key, bytes(data, "utf-8")) + res_json = json.loads(res) + assert res_json.get("weld").get("eid") + assert res_json.get("forge").get("eid") + + def test_decrypt_real_test_data_str(forge_complete_payload, private_key): + """Test decrypting when `data` is a `str`.""" + data = forge_complete_payload.get("data") + res = encryption.decrypt_rsa(private_key, data) + res_json = json.loads(res) + assert res_json.get("weld").get("eid") + assert res_json.get("forge").get("eid") + + def test_decrypt_real_test_data_all_str(forge_complete_payload, private_key): + """Test decrypting when `data` and `private_key` is a `str`.""" + data = forge_complete_payload.get("data") + + # Convert data into `bytes` since the fixture parses the file's + # JSON, converting everything into a `str`. + res = encryption.decrypt_rsa(private_key.decode(), data) + res_json = json.loads(res) + assert res_json.get("weld").get("eid") + assert res_json.get("forge").get("eid") + + def describe_encrypt_rsa(): + def test_encrypt_bytes_padded(public_key): + # This message should get padded + message = b"some message" + res = encryption.encrypt_rsa(public_key, message, auto_padding=True) + splits = res.split(b":") + assert len(splits) == 3 + for item in splits: + # Simple check to see if each item at least has something + assert bool(item) is True + + def test_encrypt_bytes_not_padded_fail(public_key): + # This message will not get padded, so it will throw an error since + # the length isn't a multiple of the block length. + message = b"some message" + + def func(): + encryption.encrypt_rsa(public_key, message, auto_padding=False) + + with pytest.raises(ValueError): + func() + + def test_encrypt_bytes_not_padded_ok(public_key): + # This message will not get padded, but meets the length requirement. + message = b"some message1111" + res = encryption.encrypt_rsa(public_key, message, auto_padding=False) + splits = res.split(b":") + assert len(splits) == 3 + + def describe_encrypt_aes(): + message = b"Secret message" + aes_key = encryption.generate_aes_key() + aes_encrypted_message = encryption.encrypt_aes(aes_key, message) + # The aes key in the first parameter is required to be in a hex + # byte string format. + decrypted_message = encryption.decrypt_aes( + aes_key.hex().encode(), aes_encrypted_message + ) + assert decrypted_message == message diff --git a/python_anvil_encryption/tests/test_utils.py b/python_anvil_encryption/tests/test_utils.py deleted file mode 100644 index c294ade..0000000 --- a/python_anvil_encryption/tests/test_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Sample unit test module using pytest-describe and expecter.""" -# pylint: disable=redefined-outer-name,unused-variable,expression-not-assigned,singleton-comparison - -from python_anvil_encryption import utils - - -def describe_feet_to_meters(): - def when_integer(): - assert utils.feet_to_meters(42) == 12.80165 - - def when_string(): - assert utils.feet_to_meters("hello") is None diff --git a/python_anvil_encryption/utils.py b/python_anvil_encryption/utils.py deleted file mode 100644 index 7d7ed8d..0000000 --- a/python_anvil_encryption/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -"""A sample module.""" - -import log - - -def feet_to_meters(feet): - """Convert feet to meters.""" - try: - value = float(feet) - except ValueError: - log.error("Unable to convert to float: %s", feet) - return None - else: - return (0.3048 * value * 10000.0 + 0.5) / 10000.0 diff --git a/tests/test_cli.py b/tests/test_cli.py index 13d5b21..d935e6b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,7 @@ import pytest from click.testing import CliRunner -from python_anvil_encryption.cli import main +# from python_anvil_encryption.cli import main @pytest.fixture @@ -12,16 +12,16 @@ def runner(): return CliRunner() -def describe_cli(): - def describe_conversion(): - def when_integer(runner): - result = runner.invoke(main, ["42"]) - - assert result.exit_code == 0 - assert result.output == "12.80165\n" - - def when_invalid(runner): - result = runner.invoke(main, ["foobar"]) - - assert result.exit_code == 0 - assert result.output == "" +# def describe_cli(): +# def describe_conversion(): +# def when_integer(runner): +# result = runner.invoke(main, ["42"]) +# +# assert result.exit_code == 0 +# assert result.output == "12.80165\n" +# +# def when_invalid(runner): +# result = runner.invoke(main, ["foobar"]) +# +# assert result.exit_code == 0 +# assert result.output == ""