├── .flake8 ├── .github └── workflows │ ├── docs.yml │ └── tests.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── defining_new_types.md ├── img │ └── bowl-mix.svg ├── index.md └── types.md ├── mkdocs.yml ├── mypy.ini ├── noxfile.py ├── overrides └── main.html ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── pytest.ini ├── src └── borsh_construct │ ├── __init__.py │ ├── core.py │ ├── enum.py │ └── py.typed └── tests ├── __init__.py ├── conftest.py ├── test_core.py └── test_hypothesis.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | D100, 4 | D104, 5 | D107, 6 | I001, 7 | I003, 8 | I004, 9 | I005, 10 | N814, 11 | Q000, 12 | WPS110, 13 | WPS111, 14 | WPS115, 15 | WPS201, 16 | WPS202, 17 | WPS221, 18 | WPS235, 19 | WPS226, 20 | WPS300, 21 | WPS301, 22 | WPS306, 23 | WPS305, 24 | WPS326, 25 | WPS410, 26 | WPS412, 27 | WPS430, 28 | WPS432, 29 | WPS441, 30 | W503, 31 | DAR201, 32 | DAR401 33 | exclude = .git,__pycache__,docs/source/conf.py,old,build,dist 34 | max-line-length = 88 35 | per-file-ignores= 36 | src/borsh_construct/core.py:F401 37 | tests/test_core.py:S101,DAR101 38 | tests/test_hypothesis.py:S101,DAR101,B008,WPS404 -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Deploy docs 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout main 13 | uses: actions/checkout@v2 14 | 15 | - name: Deploy docs 16 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: 3.9 20 | 21 | - name: Run image 22 | uses: abatilo/actions-poetry@v2.0.0 23 | with: 24 | poetry-version: 1.1.7 25 | 26 | - name: Install nox-poetry 27 | run: pip install nox-poetry 28 | 29 | - name: Run tests 30 | run: nox 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.flake8Enabled": true, 3 | "python.linting.enabled": true, 4 | "python.formatting.provider": "black", 5 | "python.linting.mypyEnabled": true, 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0] - 2021-10-01 4 | 5 | Initial release 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Kevin Heavey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # borsh-construct 2 | 3 | [![Tests](https://github.com/near/borsh-construct-py/workflows/Tests/badge.svg)](https://github.com/near/borsh-construct-py/actions?workflow=Tests) 4 | [![Docs](https://github.com/near/borsh-construct-py/workflows/Docs/badge.svg)](https://near.github.io/borsh-construct-py/) 5 | 6 | `borsh-construct` is an implementation of the [Borsh](https://borsh.io/) binary serialization format for Python projects. 7 | 8 | Borsh stands for Binary Object Representation Serializer for Hashing. It is meant to be used in security-critical projects as it prioritizes consistency, safety, speed, and comes with a strict specification. 9 | 10 | Read the [Documentation](https://near.github.io/borsh-construct-py/). 11 | ## Installation 12 | 13 | ```sh 14 | pip install borsh-construct 15 | 16 | ``` 17 | 18 | 19 | ### Development Setup 20 | 21 | 1. Install [poetry](https://python-poetry.org/docs/#installation) 22 | 2. Install dev dependencies: 23 | ```sh 24 | poetry install 25 | 26 | ``` 27 | 3. Install [nox-poetry](https://github.com/cjolowicz/nox-poetry) (note: do not use Poetry to install this, see [here](https://medium.com/@cjolowicz/nox-is-a-part-of-your-global-developer-environment-like-poetry-pre-commit-pyenv-or-pipx-1cdeba9198bd)) 28 | 4. Activate the poetry shell: 29 | ```sh 30 | poetry shell 31 | 32 | ``` 33 | 34 | ### Quick Tests 35 | ```sh 36 | pytest 37 | 38 | ``` 39 | 40 | ### Full Tests 41 | ```sh 42 | nox 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/defining_new_types.md: -------------------------------------------------------------------------------- 1 | # Defining New Types 2 | 3 | You can build new schemas on top of `borsh-construct` using [`construct.Adapter`](https://construct.readthedocs.io/en/latest/adapters.html#adapting). 4 | 5 | For example, here we implement (de)serialization for Python's `Fraction` class: 6 | 7 | ```python 8 | from typing import Tuple 9 | from fractions import Fraction 10 | from construct import Adapter 11 | from borsh_construct import I32, TupleStruct 12 | 13 | 14 | class Frac(Adapter): 15 | def __init__(self, int_type) -> None: 16 | super().__init__(TupleStruct(int_type, int_type)) # type: ignore 17 | 18 | def _encode(self, obj: Fraction, context, path) -> Tuple[int, int]: 19 | return obj.numerator, obj.denominator 20 | 21 | def _decode(self, obj: Tuple[int, int], context, path) -> Fraction: 22 | numerator, denominator = obj 23 | return Fraction(numerator, denominator) 24 | 25 | frac = Frac(I32) 26 | to_serialize = Fraction(10, 3) 27 | assert frac.parse(frac.build(to_serialize)) == to_serialize 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/img/bowl-mix.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `borsh-construct` is an implementation of the [Borsh](https://borsh.io/) binary serialization format for Python projects. 4 | 5 | Borsh stands for Binary Object Representation Serializer for Hashing. It is meant to be used in security-critical projects as it prioritizes consistency, safety, speed, and comes with a strict specification. 6 | 7 | `borsh-construct` is built on top of the very powerful [`construct`](https://construct.readthedocs.io/en/latest/) library, so it is strongly recommended that you have a look at the basics of `construct` before using `borsh-construct`. 8 | 9 | ## Who is this for? 10 | 11 | This library was built with the NEAR and Solana blockchains in mind: the only obvious reason to use `borsh-construct` currently is to write client code for those blockchains, as they typically expect Borsh serialization. 12 | 13 | You may find other reasons to use `borsh-construct`, but it is worth noting that the Borsh spec is written from a Rust perspective, so if you're not interacting with a Rust project then Borsh may not make sense. 14 | 15 | ## Installation 16 | 17 | `pip install borsh-construct` 18 | 19 | Alternatively, using conda/mamba: 20 | 21 | `mamba install borsh-construct -c conda-forge` 22 | 23 | ## Usage 24 | 25 | Since `borsh-construct` is built with `construct`, it serializes objects using `.build` and deserializes them using `.parse`. For example: 26 | 27 | ```python 28 | >>> from borsh_construct import U8, String, CStruct 29 | >>> animal = CStruct( 30 | ... "name" / String, 31 | ... "legs" / U8 32 | ... ) 33 | >>> animal.build({"name": "Ferris", "legs": 6}) 34 | b'\x06\x00\x00\x00Ferris\x06' 35 | >>> animal.parse(b'\x06\x00\x00\x00Ferris\x06') 36 | Container(name=u'Ferris', legs=6) 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/types.md: -------------------------------------------------------------------------------- 1 | This is an outline of all the types supported by `borsh-construct`. Since Borsh is Rust-centric, some Rust snippets are included to make it clear what the equivalent Rust type is. 2 | 3 | ## Numeric types 4 | 5 | All numeric types mentioned in the Borsh spec are supported: 6 | 7 | - Unsigned integers: U8, U16, U32, U64, U128 8 | - Signed integers: I8, I16, I32, I64, I128 9 | - Floats: F32, F64 10 | - Bool (this is not explicitly part of the spec, but `borsh-rs` implements Bool as a `u8` with value 0 or 1) 11 | 12 | Example: 13 | 14 | ```python 15 | >>> from borsh_construct import U32 16 | >>> U32.build(42) 17 | b'*\x00\x00\x00' 18 | >>> U32.parse(b'*\x00\x00\x00') 19 | 42 20 | 21 | ``` 22 | 23 | 24 | !!! note 25 | 26 | Most of the numeric types come directly from the `construct` library, and are just aliased so that they match the Borsh spec. For example, `borsh_construct.U8` is really `construct.Int8ul`. 27 | 28 | ## Fixed sized arrays 29 | 30 | `construct` gives us a nice `[]` syntax to represent fixed sized arrays. For example, an array of 3 u8 integers: 31 | 32 | ```python 33 | >>> from borsh_construct import U8 34 | >>> U8[3].build([1, 2, 3]) 35 | b'\x01\x02\x03' 36 | >>> U8[3].parse(b'\x01\x02\x03') 37 | ListContainer([1, 2, 3]) 38 | 39 | ``` 40 | 41 | This is what that fixed size array looks like in Rust: 42 | 43 | ```rust 44 | let arr = [1u8, 2, 3]; 45 | 46 | ``` 47 | 48 | ## Dynamic sized arrays 49 | 50 | Dynamic arrays are implemented using the `Vec` function: 51 | 52 | ```python 53 | >>> from borsh_construct import Vec, U8 54 | >>> Vec(U8).build([1, 2, 3]) 55 | b'\x03\x00\x00\x00\x01\x02\x03' 56 | >>> Vec(U8).parse(b'\x03\x00\x00\x00\x01\x02\x03') 57 | ListContainer([1, 2, 3]) 58 | 59 | ``` 60 | 61 | In Rust we could build that vector like this: 62 | 63 | ```rust 64 | let v = vec![1u8, 2, 3]; 65 | 66 | ``` 67 | 68 | ## C-like structs 69 | 70 | This is analogous to a Rust struct with named fields: 71 | 72 | ```python 73 | >>> from borsh_construct import CStruct, String, U8 74 | >>> person = CStruct( 75 | ... "name" / String, 76 | ... "age" / U8 77 | ... ) 78 | >>> person.build({"name": "Alice", "age": 50}) 79 | b'\x05\x00\x00\x00Alice2' 80 | >>> person.parse(b'\x05\x00\x00\x00Alice2') 81 | Container(name=u'Alice', age=50) 82 | 83 | ``` 84 | Rust type: 85 | ```rust 86 | struct Person { 87 | name: String, 88 | age: u8, 89 | } 90 | 91 | ``` 92 | 93 | ## Tuple structs 94 | ```python 95 | >>> from borsh_construct import TupleStruct, I32, F32 96 | >>> pair = TupleStruct(I32, F32) 97 | >>> pair.build([3, 0.5]) 98 | b'\x03\x00\x00\x00\x00\x00\x00?' 99 | >>> pair.parse(b'\x03\x00\x00\x00\x00\x00\x00?') 100 | ListContainer([3, 0.5]) 101 | 102 | ``` 103 | Rust type: 104 | ```rust 105 | struct Pair(i32, f32); 106 | 107 | ``` 108 | 109 | 110 | ## Enum 111 | 112 | Rust's `enum` is the trickiest part of `borsh-construct` because it's rather different from Python's `enum.Enum`. Under the hood, `borsh-construct` uses the [`sumtypes`](https://sumtypes.readthedocs.io/en/latest/) library to represent Rust enums in Python. 113 | 114 | Notice below how our `message` object has a `.enum` attribute: this is the Python imitation of Rust's enum type. 115 | 116 | Defining an enum: 117 | 118 | ```python 119 | >>> from borsh_construct import Enum, I32, CStruct, TupleStruct, String 120 | >>> message = Enum( 121 | ... "Quit", 122 | ... "Move" / CStruct("x" / I32, "y" / I32), 123 | ... "Write" / TupleStruct(String), 124 | ... "ChangeColor" / TupleStruct(I32, I32, I32), 125 | ... enum_name="Message", 126 | ... ) 127 | >>> message.build(message.enum.Quit()) 128 | b'\x00' 129 | >>> message.parse(b'\x00') 130 | Message.Quit() 131 | >>> message.build(message.enum.Move(x=1, y=3)) 132 | b'\x01\x01\x00\x00\x00\x03\x00\x00\x00' 133 | >>> message.parse(b'\x01\x01\x00\x00\x00\x03\x00\x00\x00') 134 | Message.Move(x=1, y=3) 135 | >>> message.build(message.enum.Write(("hello",))) 136 | b'\x02\x05\x00\x00\x00hello' 137 | >>> message.parse(b'\x02\x05\x00\x00\x00hello') 138 | Message.Write(tuple_data=ListContainer(['hello'])) 139 | >>> message.build(message.enum.ChangeColor((1, 2, 3))) 140 | b'\x03\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00' 141 | >>> message.parse(b'\x03\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00') 142 | Message.ChangeColor(tuple_data=ListContainer([1, 2, 3])) 143 | 144 | ``` 145 | 146 | Notice also how each variant of the enum is a subclass of the enum itself: 147 | 148 | ```python 149 | >>> assert isinstance(message.enum.Quit(), message.enum) 150 | 151 | ``` 152 | 153 | Rust type: 154 | ```rust 155 | enum Message { 156 | Quit, 157 | Move { x: i32, y: i32 }, 158 | Write(String), 159 | ChangeColor(i32, i32, i32), 160 | } 161 | ``` 162 | 163 | ## HashMap 164 | 165 | You can think of HashMap as a Python dictionary as long as the keys and values have a well-defined type. 166 | 167 | ```python 168 | >>> from borsh_construct import HashMap, String, U32 169 | >>> scores = HashMap(String, U32) 170 | >>> scores.build({"Blue": 10, "Yellow": 50}) 171 | b'\x02\x00\x00\x00\x04\x00\x00\x00Blue\n\x00\x00\x00\x06\x00\x00\x00Yellow2\x00\x00\x00' 172 | >>> scores.parse(b'\x02\x00\x00\x00\x04\x00\x00\x00Blue\n\x00\x00\x00\x06\x00\x00\x00Yellow2\x00\x00\x00') 173 | {'Blue': 10, 'Yellow': 50} 174 | 175 | ``` 176 | Rust type: 177 | ```rust 178 | fn main() { 179 | use std::collections::HashMap; 180 | 181 | let mut scores = HashMap::new(); 182 | 183 | scores.insert(String::from("Blue"), 10); 184 | scores.insert(String::from("Yellow"), 50); 185 | } 186 | ``` 187 | 188 | ## HashSet 189 | 190 | The HashSet is similar to a Python `set` with a well-defined type. 191 | 192 | ```python 193 | >>> from borsh_construct import HashSet, I32 194 | >>> a = HashSet(I32) 195 | >>> a.build({1, 2, 3}) 196 | b'\x03\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00' 197 | >>> a.parse(b'\x03\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00') 198 | {1, 2, 3} 199 | 200 | ``` 201 | Rust type: 202 | ```rust 203 | use std::collections::HashSet; 204 | 205 | fn main() { 206 | let mut a: HashSet = vec![1i32, 2, 3].into_iter().collect(); 207 | } 208 | 209 | ``` 210 | 211 | ## Option 212 | 213 | Rust programmers will notice that our Option type is not implemented like a Rust enum, because it's not worth the complexity. 214 | 215 | ```python 216 | >>> from borsh_construct import Option, U8 217 | >>> optional_num = Option(U8) 218 | >>> optional_num.build(None) 219 | b'\x00' 220 | >>> optional_num.parse(b'\x00') is None 221 | True 222 | >>> optional_num.build(3) 223 | b'\x01\x03' 224 | >>> optional_num.parse(b'\x01\x03') 225 | 3 226 | 227 | ``` 228 | Rust type: 229 | ```rust 230 | Option 231 | ``` 232 | 233 | ## Bytes 234 | 235 | The Borsh spec doesn't specifically mention serializing raw bytes, but it's worth including anyway: 236 | 237 | ```python 238 | >>> from borsh_construct import Bytes 239 | >>> Bytes.build(bytes([1, 2, 3])) 240 | b'\x03\x00\x00\x00\x01\x02\x03' 241 | >>> Bytes.parse(b'\x03\x00\x00\x00\x01\x02\x03') 242 | b'\x01\x02\x03' 243 | 244 | ``` 245 | Rust type: 246 | ```rust 247 | vec![1u8, 2, 3] 248 | 249 | ``` 250 | 251 | ## String 252 | 253 | Python: 254 | 255 | ```python 256 | >>> from borsh_construct import String 257 | >>> String.build("🚀🚀🚀") 258 | b'\x0c\x00\x00\x00\xf0\x9f\x9a\x80\xf0\x9f\x9a\x80\xf0\x9f\x9a\x80' 259 | >>> String.parse(b'\x0c\x00\x00\x00\xf0\x9f\x9a\x80\xf0\x9f\x9a\x80\xf0\x9f\x9a\x80') 260 | '🚀🚀🚀' 261 | 262 | ``` 263 | 264 | Rust type: 265 | 266 | ```rust 267 | String::from("🚀🚀🚀") 268 | 269 | ``` 270 | 271 | 272 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: borsh-construct 2 | theme: 3 | name: material 4 | icon: 5 | logo: material/bowl-mix 6 | favicon: img/bowl-mix.svg 7 | palette: 8 | - scheme: default 9 | primary: lime 10 | toggle: 11 | icon: material/toggle-switch-off-outline 12 | name: Switch to dark mode 13 | - scheme: slate 14 | toggle: 15 | icon: material/toggle-switch 16 | name: Switch to light mode 17 | custom_dir: overrides 18 | markdown_extensions: 19 | - pymdownx.highlight 20 | - pymdownx.superfences 21 | - admonition 22 | repo_url: https://github.com/near/borsh-construct-py 23 | repo_name: near/borsh-construct-py 24 | site_url: https://near.github.io/borsh-construct-py/ 25 | nav: 26 | - index.md 27 | - types.md 28 | - defining_new_types.md 29 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | [mypy-pytest] 4 | ignore_missing_imports = True 5 | 6 | [mypy-sumtypes] 7 | ignore_missing_imports = True 8 | 9 | [mypy-nox_poetry] 10 | ignore_missing_imports = True 11 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | from nox_poetry import session # type: ignore 2 | 3 | 4 | @session(python=["3.9", "3.8.3"]) 5 | def tests(session): # noqa: D103,WPS442 6 | session.run_always("poetry", "install", external=True) 7 | session.install(".") 8 | session.run("pytest", external=True) 9 | -------------------------------------------------------------------------------- /overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | {% endblock %} -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astor" 3 | version = "0.8.1" 4 | description = "Read/rewrite/write Python ASTs" 5 | category = "dev" 6 | optional = false 7 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 8 | 9 | [[package]] 10 | name = "atomicwrites" 11 | version = "1.4.0" 12 | description = "Atomic file writes." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "21.2.0" 20 | description = "Classes Without Boilerplate" 21 | category = "main" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 24 | 25 | [package.extras] 26 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 27 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 28 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 29 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 30 | 31 | [[package]] 32 | name = "bandit" 33 | version = "1.7.0" 34 | description = "Security oriented static analyser for python code." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=3.5" 38 | 39 | [package.dependencies] 40 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 41 | GitPython = ">=1.0.1" 42 | PyYAML = ">=5.3.1" 43 | six = ">=1.10.0" 44 | stevedore = ">=1.20.0" 45 | 46 | [[package]] 47 | name = "black" 48 | version = "21.9b0" 49 | description = "The uncompromising code formatter." 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=3.6.2" 53 | 54 | [package.dependencies] 55 | click = ">=7.1.2" 56 | mypy-extensions = ">=0.4.3" 57 | pathspec = ">=0.9.0,<1" 58 | platformdirs = ">=2" 59 | regex = ">=2020.1.8" 60 | tomli = ">=0.2.6,<2.0.0" 61 | typing-extensions = [ 62 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 63 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 64 | ] 65 | 66 | [package.extras] 67 | colorama = ["colorama (>=0.4.3)"] 68 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 69 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 70 | python2 = ["typed-ast (>=1.4.2)"] 71 | uvloop = ["uvloop (>=0.15.2)"] 72 | 73 | [[package]] 74 | name = "click" 75 | version = "8.0.3" 76 | description = "Composable command line interface toolkit" 77 | category = "dev" 78 | optional = false 79 | python-versions = ">=3.6" 80 | 81 | [package.dependencies] 82 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 83 | 84 | [[package]] 85 | name = "colorama" 86 | version = "0.4.4" 87 | description = "Cross-platform colored terminal text." 88 | category = "dev" 89 | optional = false 90 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 91 | 92 | [[package]] 93 | name = "construct" 94 | version = "2.10.67" 95 | description = "A powerful declarative symmetric parser/builder for binary data" 96 | category = "main" 97 | optional = false 98 | python-versions = ">=3.6" 99 | 100 | [package.extras] 101 | extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] 102 | 103 | [[package]] 104 | name = "construct-typing" 105 | version = "0.5.1" 106 | description = "Extension for the python package 'construct' that adds typing features" 107 | category = "main" 108 | optional = false 109 | python-versions = ">=3.7" 110 | 111 | [package.dependencies] 112 | construct = "2.10.67" 113 | 114 | [[package]] 115 | name = "coverage" 116 | version = "5.5" 117 | description = "Code coverage measurement for Python" 118 | category = "dev" 119 | optional = false 120 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 121 | 122 | [package.dependencies] 123 | toml = {version = "*", optional = true, markers = "extra == \"toml\""} 124 | 125 | [package.extras] 126 | toml = ["toml"] 127 | 128 | [[package]] 129 | name = "darglint" 130 | version = "1.8.0" 131 | description = "A utility for ensuring Google-style docstrings stay up to date with the source code." 132 | category = "dev" 133 | optional = false 134 | python-versions = ">=3.6,<4.0" 135 | 136 | [[package]] 137 | name = "docutils" 138 | version = "0.17.1" 139 | description = "Docutils -- Python Documentation Utilities" 140 | category = "dev" 141 | optional = false 142 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 143 | 144 | [[package]] 145 | name = "eradicate" 146 | version = "2.0.0" 147 | description = "Removes commented-out code." 148 | category = "dev" 149 | optional = false 150 | python-versions = "*" 151 | 152 | [[package]] 153 | name = "filelock" 154 | version = "3.3.1" 155 | description = "A platform independent file lock." 156 | category = "dev" 157 | optional = false 158 | python-versions = ">=3.6" 159 | 160 | [package.extras] 161 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 162 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 163 | 164 | [[package]] 165 | name = "flake8" 166 | version = "3.9.2" 167 | description = "the modular source code checker: pep8 pyflakes and co" 168 | category = "dev" 169 | optional = false 170 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 171 | 172 | [package.dependencies] 173 | mccabe = ">=0.6.0,<0.7.0" 174 | pycodestyle = ">=2.7.0,<2.8.0" 175 | pyflakes = ">=2.3.0,<2.4.0" 176 | 177 | [[package]] 178 | name = "flake8-bandit" 179 | version = "2.1.2" 180 | description = "Automated security testing with bandit and flake8." 181 | category = "dev" 182 | optional = false 183 | python-versions = "*" 184 | 185 | [package.dependencies] 186 | bandit = "*" 187 | flake8 = "*" 188 | flake8-polyfill = "*" 189 | pycodestyle = "*" 190 | 191 | [[package]] 192 | name = "flake8-broken-line" 193 | version = "0.3.0" 194 | description = "Flake8 plugin to forbid backslashes for line breaks" 195 | category = "dev" 196 | optional = false 197 | python-versions = ">=3.6,<4.0" 198 | 199 | [package.dependencies] 200 | flake8 = ">=3.5,<4.0" 201 | 202 | [[package]] 203 | name = "flake8-bugbear" 204 | version = "21.9.2" 205 | description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." 206 | category = "dev" 207 | optional = false 208 | python-versions = ">=3.6" 209 | 210 | [package.dependencies] 211 | attrs = ">=19.2.0" 212 | flake8 = ">=3.0.0" 213 | 214 | [package.extras] 215 | dev = ["coverage", "black", "hypothesis", "hypothesmith"] 216 | 217 | [[package]] 218 | name = "flake8-commas" 219 | version = "2.1.0" 220 | description = "Flake8 lint for trailing commas." 221 | category = "dev" 222 | optional = false 223 | python-versions = "*" 224 | 225 | [package.dependencies] 226 | flake8 = ">=2" 227 | 228 | [[package]] 229 | name = "flake8-comprehensions" 230 | version = "3.7.0" 231 | description = "A flake8 plugin to help you write better list/set/dict comprehensions." 232 | category = "dev" 233 | optional = false 234 | python-versions = ">=3.6" 235 | 236 | [package.dependencies] 237 | flake8 = ">=3.0,<3.2.0 || >3.2.0,<5" 238 | 239 | [[package]] 240 | name = "flake8-debugger" 241 | version = "4.0.0" 242 | description = "ipdb/pdb statement checker plugin for flake8" 243 | category = "dev" 244 | optional = false 245 | python-versions = ">=3.6" 246 | 247 | [package.dependencies] 248 | flake8 = ">=3.0" 249 | pycodestyle = "*" 250 | six = "*" 251 | 252 | [[package]] 253 | name = "flake8-docstrings" 254 | version = "1.6.0" 255 | description = "Extension for flake8 which uses pydocstyle to check docstrings" 256 | category = "dev" 257 | optional = false 258 | python-versions = "*" 259 | 260 | [package.dependencies] 261 | flake8 = ">=3" 262 | pydocstyle = ">=2.1" 263 | 264 | [[package]] 265 | name = "flake8-eradicate" 266 | version = "1.1.0" 267 | description = "Flake8 plugin to find commented out code" 268 | category = "dev" 269 | optional = false 270 | python-versions = ">=3.6,<4.0" 271 | 272 | [package.dependencies] 273 | attrs = "*" 274 | eradicate = ">=2.0,<3.0" 275 | flake8 = ">=3.5,<4.0" 276 | 277 | [[package]] 278 | name = "flake8-isort" 279 | version = "4.1.1" 280 | description = "flake8 plugin that integrates isort ." 281 | category = "dev" 282 | optional = false 283 | python-versions = "*" 284 | 285 | [package.dependencies] 286 | flake8 = ">=3.2.1,<5" 287 | isort = ">=4.3.5,<6" 288 | testfixtures = ">=6.8.0,<7" 289 | 290 | [package.extras] 291 | test = ["pytest-cov"] 292 | 293 | [[package]] 294 | name = "flake8-polyfill" 295 | version = "1.0.2" 296 | description = "Polyfill package for Flake8 plugins" 297 | category = "dev" 298 | optional = false 299 | python-versions = "*" 300 | 301 | [package.dependencies] 302 | flake8 = "*" 303 | 304 | [[package]] 305 | name = "flake8-quotes" 306 | version = "3.3.0" 307 | description = "Flake8 lint for quotes." 308 | category = "dev" 309 | optional = false 310 | python-versions = "*" 311 | 312 | [package.dependencies] 313 | flake8 = "*" 314 | 315 | [[package]] 316 | name = "flake8-rst-docstrings" 317 | version = "0.2.3" 318 | description = "Python docstring reStructuredText (RST) validator" 319 | category = "dev" 320 | optional = false 321 | python-versions = ">=3.3" 322 | 323 | [package.dependencies] 324 | flake8 = ">=3.0.0" 325 | pygments = "*" 326 | restructuredtext-lint = "*" 327 | 328 | [[package]] 329 | name = "flake8-string-format" 330 | version = "0.3.0" 331 | description = "string format checker, plugin for flake8" 332 | category = "dev" 333 | optional = false 334 | python-versions = "*" 335 | 336 | [package.dependencies] 337 | flake8 = "*" 338 | 339 | [[package]] 340 | name = "ghp-import" 341 | version = "2.0.2" 342 | description = "Copy your docs directly to the gh-pages branch." 343 | category = "dev" 344 | optional = false 345 | python-versions = "*" 346 | 347 | [package.dependencies] 348 | python-dateutil = ">=2.8.1" 349 | 350 | [package.extras] 351 | dev = ["twine", "markdown", "flake8", "wheel"] 352 | 353 | [[package]] 354 | name = "gitdb" 355 | version = "4.0.7" 356 | description = "Git Object Database" 357 | category = "dev" 358 | optional = false 359 | python-versions = ">=3.4" 360 | 361 | [package.dependencies] 362 | smmap = ">=3.0.1,<5" 363 | 364 | [[package]] 365 | name = "gitpython" 366 | version = "3.1.24" 367 | description = "GitPython is a python library used to interact with Git repositories" 368 | category = "dev" 369 | optional = false 370 | python-versions = ">=3.7" 371 | 372 | [package.dependencies] 373 | gitdb = ">=4.0.1,<5" 374 | typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} 375 | 376 | [[package]] 377 | name = "hypothesis" 378 | version = "6.23.2" 379 | description = "A library for property-based testing" 380 | category = "dev" 381 | optional = false 382 | python-versions = ">=3.6" 383 | 384 | [package.dependencies] 385 | attrs = ">=19.2.0" 386 | sortedcontainers = ">=2.1.0,<3.0.0" 387 | 388 | [package.extras] 389 | all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] 390 | cli = ["click (>=7.0)", "black (>=19.10b0)", "rich (>=9.0.0)"] 391 | codemods = ["libcst (>=0.3.16)"] 392 | dateutil = ["python-dateutil (>=1.4)"] 393 | django = ["pytz (>=2014.1)", "django (>=2.2)"] 394 | dpcontracts = ["dpcontracts (>=0.4)"] 395 | ghostwriter = ["black (>=19.10b0)"] 396 | lark = ["lark-parser (>=0.6.5)"] 397 | numpy = ["numpy (>=1.9.0)"] 398 | pandas = ["pandas (>=0.25)"] 399 | pytest = ["pytest (>=4.6)"] 400 | pytz = ["pytz (>=2014.1)"] 401 | redis = ["redis (>=3.0.0)"] 402 | zoneinfo = ["importlib-resources (>=3.3.0)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] 403 | 404 | [[package]] 405 | name = "importlib-metadata" 406 | version = "4.8.1" 407 | description = "Read metadata from Python packages" 408 | category = "dev" 409 | optional = false 410 | python-versions = ">=3.6" 411 | 412 | [package.dependencies] 413 | zipp = ">=0.5" 414 | 415 | [package.extras] 416 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 417 | perf = ["ipython"] 418 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 419 | 420 | [[package]] 421 | name = "isort" 422 | version = "5.9.3" 423 | description = "A Python utility / library to sort Python imports." 424 | category = "dev" 425 | optional = false 426 | python-versions = ">=3.6.1,<4.0" 427 | 428 | [package.extras] 429 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 430 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 431 | colors = ["colorama (>=0.4.3,<0.5.0)"] 432 | plugins = ["setuptools"] 433 | 434 | [[package]] 435 | name = "jinja2" 436 | version = "3.0.2" 437 | description = "A very fast and expressive template engine." 438 | category = "dev" 439 | optional = false 440 | python-versions = ">=3.6" 441 | 442 | [package.dependencies] 443 | MarkupSafe = ">=2.0" 444 | 445 | [package.extras] 446 | i18n = ["Babel (>=2.7)"] 447 | 448 | [[package]] 449 | name = "markdown" 450 | version = "3.3.4" 451 | description = "Python implementation of Markdown." 452 | category = "dev" 453 | optional = false 454 | python-versions = ">=3.6" 455 | 456 | [package.extras] 457 | testing = ["coverage", "pyyaml"] 458 | 459 | [[package]] 460 | name = "markupsafe" 461 | version = "2.0.1" 462 | description = "Safely add untrusted strings to HTML/XML markup." 463 | category = "dev" 464 | optional = false 465 | python-versions = ">=3.6" 466 | 467 | [[package]] 468 | name = "mccabe" 469 | version = "0.6.1" 470 | description = "McCabe checker, plugin for flake8" 471 | category = "dev" 472 | optional = false 473 | python-versions = "*" 474 | 475 | [[package]] 476 | name = "mergedeep" 477 | version = "1.3.4" 478 | description = "A deep merge function for 🐍." 479 | category = "dev" 480 | optional = false 481 | python-versions = ">=3.6" 482 | 483 | [[package]] 484 | name = "mkdocs" 485 | version = "1.2.3" 486 | description = "Project documentation with Markdown." 487 | category = "dev" 488 | optional = false 489 | python-versions = ">=3.6" 490 | 491 | [package.dependencies] 492 | click = ">=3.3" 493 | ghp-import = ">=1.0" 494 | importlib-metadata = ">=3.10" 495 | Jinja2 = ">=2.10.1" 496 | Markdown = ">=3.2.1" 497 | mergedeep = ">=1.3.4" 498 | packaging = ">=20.5" 499 | PyYAML = ">=3.10" 500 | pyyaml-env-tag = ">=0.1" 501 | watchdog = ">=2.0" 502 | 503 | [package.extras] 504 | i18n = ["babel (>=2.9.0)"] 505 | 506 | [[package]] 507 | name = "mkdocs-material" 508 | version = "7.3.3" 509 | description = "A Material Design theme for MkDocs" 510 | category = "dev" 511 | optional = false 512 | python-versions = "*" 513 | 514 | [package.dependencies] 515 | jinja2 = ">=2.11.1" 516 | markdown = ">=3.2" 517 | mkdocs = ">=1.2.2" 518 | mkdocs-material-extensions = ">=1.0" 519 | pygments = ">=2.4" 520 | pymdown-extensions = ">=9.0" 521 | 522 | [[package]] 523 | name = "mkdocs-material-extensions" 524 | version = "1.0.3" 525 | description = "Extension pack for Python Markdown." 526 | category = "dev" 527 | optional = false 528 | python-versions = ">=3.6" 529 | 530 | [[package]] 531 | name = "more-itertools" 532 | version = "8.10.0" 533 | description = "More routines for operating on iterables, beyond itertools" 534 | category = "dev" 535 | optional = false 536 | python-versions = ">=3.5" 537 | 538 | [[package]] 539 | name = "mypy" 540 | version = "0.910" 541 | description = "Optional static typing for Python" 542 | category = "dev" 543 | optional = false 544 | python-versions = ">=3.5" 545 | 546 | [package.dependencies] 547 | mypy-extensions = ">=0.4.3,<0.5.0" 548 | toml = "*" 549 | typing-extensions = ">=3.7.4" 550 | 551 | [package.extras] 552 | dmypy = ["psutil (>=4.0)"] 553 | python2 = ["typed-ast (>=1.4.0,<1.5.0)"] 554 | 555 | [[package]] 556 | name = "mypy-extensions" 557 | version = "0.4.3" 558 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 559 | category = "dev" 560 | optional = false 561 | python-versions = "*" 562 | 563 | [[package]] 564 | name = "packaging" 565 | version = "21.0" 566 | description = "Core utilities for Python packages" 567 | category = "dev" 568 | optional = false 569 | python-versions = ">=3.6" 570 | 571 | [package.dependencies] 572 | pyparsing = ">=2.0.2" 573 | 574 | [[package]] 575 | name = "pathspec" 576 | version = "0.9.0" 577 | description = "Utility library for gitignore style pattern matching of file paths." 578 | category = "dev" 579 | optional = false 580 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 581 | 582 | [[package]] 583 | name = "pbr" 584 | version = "5.6.0" 585 | description = "Python Build Reasonableness" 586 | category = "dev" 587 | optional = false 588 | python-versions = ">=2.6" 589 | 590 | [[package]] 591 | name = "pep8-naming" 592 | version = "0.11.1" 593 | description = "Check PEP-8 naming conventions, plugin for flake8" 594 | category = "dev" 595 | optional = false 596 | python-versions = "*" 597 | 598 | [package.dependencies] 599 | flake8-polyfill = ">=1.0.2,<2" 600 | 601 | [[package]] 602 | name = "platformdirs" 603 | version = "2.4.0" 604 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 605 | category = "dev" 606 | optional = false 607 | python-versions = ">=3.6" 608 | 609 | [package.extras] 610 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 611 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 612 | 613 | [[package]] 614 | name = "pluggy" 615 | version = "0.13.1" 616 | description = "plugin and hook calling mechanisms for python" 617 | category = "dev" 618 | optional = false 619 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 620 | 621 | [package.extras] 622 | dev = ["pre-commit", "tox"] 623 | 624 | [[package]] 625 | name = "py" 626 | version = "1.10.0" 627 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 628 | category = "dev" 629 | optional = false 630 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 631 | 632 | [[package]] 633 | name = "pycodestyle" 634 | version = "2.7.0" 635 | description = "Python style guide checker" 636 | category = "dev" 637 | optional = false 638 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 639 | 640 | [[package]] 641 | name = "pydocstyle" 642 | version = "6.1.1" 643 | description = "Python docstring style checker" 644 | category = "dev" 645 | optional = false 646 | python-versions = ">=3.6" 647 | 648 | [package.dependencies] 649 | snowballstemmer = "*" 650 | 651 | [package.extras] 652 | toml = ["toml"] 653 | 654 | [[package]] 655 | name = "pyflakes" 656 | version = "2.3.1" 657 | description = "passive checker of Python programs" 658 | category = "dev" 659 | optional = false 660 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 661 | 662 | [[package]] 663 | name = "pygments" 664 | version = "2.10.0" 665 | description = "Pygments is a syntax highlighting package written in Python." 666 | category = "dev" 667 | optional = false 668 | python-versions = ">=3.5" 669 | 670 | [[package]] 671 | name = "pymdown-extensions" 672 | version = "9.0" 673 | description = "Extension pack for Python Markdown." 674 | category = "dev" 675 | optional = false 676 | python-versions = ">=3.6" 677 | 678 | [package.dependencies] 679 | Markdown = ">=3.2" 680 | 681 | [[package]] 682 | name = "pyparsing" 683 | version = "2.4.7" 684 | description = "Python parsing module" 685 | category = "dev" 686 | optional = false 687 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 688 | 689 | [[package]] 690 | name = "pytest" 691 | version = "5.4.3" 692 | description = "pytest: simple powerful testing with Python" 693 | category = "dev" 694 | optional = false 695 | python-versions = ">=3.5" 696 | 697 | [package.dependencies] 698 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 699 | attrs = ">=17.4.0" 700 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 701 | more-itertools = ">=4.0.0" 702 | packaging = "*" 703 | pluggy = ">=0.12,<1.0" 704 | py = ">=1.5.0" 705 | wcwidth = "*" 706 | 707 | [package.extras] 708 | checkqa-mypy = ["mypy (==v0.761)"] 709 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 710 | 711 | [[package]] 712 | name = "pytest-cov" 713 | version = "2.12.1" 714 | description = "Pytest plugin for measuring coverage." 715 | category = "dev" 716 | optional = false 717 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 718 | 719 | [package.dependencies] 720 | coverage = ">=5.2.1" 721 | pytest = ">=4.6" 722 | toml = "*" 723 | 724 | [package.extras] 725 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 726 | 727 | [[package]] 728 | name = "pytest-flake8" 729 | version = "1.0.7" 730 | description = "pytest plugin to check FLAKE8 requirements" 731 | category = "dev" 732 | optional = false 733 | python-versions = "*" 734 | 735 | [package.dependencies] 736 | flake8 = ">=3.5" 737 | pytest = ">=3.5" 738 | 739 | [[package]] 740 | name = "pytest-mypy" 741 | version = "0.8.1" 742 | description = "Mypy static type checker plugin for Pytest" 743 | category = "dev" 744 | optional = false 745 | python-versions = ">=3.5" 746 | 747 | [package.dependencies] 748 | attrs = ">=19.0" 749 | filelock = ">=3.0" 750 | mypy = [ 751 | {version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""}, 752 | {version = ">=0.780", markers = "python_version >= \"3.9\""}, 753 | ] 754 | pytest = ">=3.5" 755 | 756 | [[package]] 757 | name = "python-dateutil" 758 | version = "2.8.2" 759 | description = "Extensions to the standard Python datetime module" 760 | category = "dev" 761 | optional = false 762 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 763 | 764 | [package.dependencies] 765 | six = ">=1.5" 766 | 767 | [[package]] 768 | name = "pyyaml" 769 | version = "6.0" 770 | description = "YAML parser and emitter for Python" 771 | category = "dev" 772 | optional = false 773 | python-versions = ">=3.6" 774 | 775 | [[package]] 776 | name = "pyyaml-env-tag" 777 | version = "0.1" 778 | description = "A custom YAML tag for referencing environment variables in YAML files. " 779 | category = "dev" 780 | optional = false 781 | python-versions = ">=3.6" 782 | 783 | [package.dependencies] 784 | pyyaml = "*" 785 | 786 | [[package]] 787 | name = "regex" 788 | version = "2021.10.8" 789 | description = "Alternative regular expression module, to replace re." 790 | category = "dev" 791 | optional = false 792 | python-versions = "*" 793 | 794 | [[package]] 795 | name = "restructuredtext-lint" 796 | version = "1.3.2" 797 | description = "reStructuredText linter" 798 | category = "dev" 799 | optional = false 800 | python-versions = "*" 801 | 802 | [package.dependencies] 803 | docutils = ">=0.11,<1.0" 804 | 805 | [[package]] 806 | name = "six" 807 | version = "1.16.0" 808 | description = "Python 2 and 3 compatibility utilities" 809 | category = "dev" 810 | optional = false 811 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 812 | 813 | [[package]] 814 | name = "smmap" 815 | version = "4.0.0" 816 | description = "A pure Python implementation of a sliding window memory map manager" 817 | category = "dev" 818 | optional = false 819 | python-versions = ">=3.5" 820 | 821 | [[package]] 822 | name = "snowballstemmer" 823 | version = "2.1.0" 824 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 825 | category = "dev" 826 | optional = false 827 | python-versions = "*" 828 | 829 | [[package]] 830 | name = "sortedcontainers" 831 | version = "2.4.0" 832 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 833 | category = "dev" 834 | optional = false 835 | python-versions = "*" 836 | 837 | [[package]] 838 | name = "stevedore" 839 | version = "3.4.0" 840 | description = "Manage dynamic plugins for Python applications" 841 | category = "dev" 842 | optional = false 843 | python-versions = ">=3.6" 844 | 845 | [package.dependencies] 846 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 847 | 848 | [[package]] 849 | name = "sumtypes" 850 | version = "0.1a5" 851 | description = "Algebraic types for Python (notably providing Sum Types, aka Tagged Unions)" 852 | category = "main" 853 | optional = false 854 | python-versions = "*" 855 | 856 | [package.dependencies] 857 | attrs = "*" 858 | 859 | [[package]] 860 | name = "testfixtures" 861 | version = "6.18.3" 862 | description = "A collection of helpers and mock objects for unit tests and doc tests." 863 | category = "dev" 864 | optional = false 865 | python-versions = "*" 866 | 867 | [package.extras] 868 | build = ["setuptools-git", "wheel", "twine"] 869 | docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] 870 | test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] 871 | 872 | [[package]] 873 | name = "toml" 874 | version = "0.10.2" 875 | description = "Python Library for Tom's Obvious, Minimal Language" 876 | category = "dev" 877 | optional = false 878 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 879 | 880 | [[package]] 881 | name = "tomli" 882 | version = "1.2.1" 883 | description = "A lil' TOML parser" 884 | category = "dev" 885 | optional = false 886 | python-versions = ">=3.6" 887 | 888 | [[package]] 889 | name = "typing-extensions" 890 | version = "3.10.0.2" 891 | description = "Backported and Experimental Type Hints for Python 3.5+" 892 | category = "dev" 893 | optional = false 894 | python-versions = "*" 895 | 896 | [[package]] 897 | name = "watchdog" 898 | version = "2.1.6" 899 | description = "Filesystem events monitoring" 900 | category = "dev" 901 | optional = false 902 | python-versions = ">=3.6" 903 | 904 | [package.extras] 905 | watchmedo = ["PyYAML (>=3.10)"] 906 | 907 | [[package]] 908 | name = "wcwidth" 909 | version = "0.2.5" 910 | description = "Measures the displayed width of unicode strings in a terminal" 911 | category = "dev" 912 | optional = false 913 | python-versions = "*" 914 | 915 | [[package]] 916 | name = "wemake-python-styleguide" 917 | version = "0.15.3" 918 | description = "The strictest and most opinionated python linter ever" 919 | category = "dev" 920 | optional = false 921 | python-versions = ">=3.6,<4.0" 922 | 923 | [package.dependencies] 924 | astor = ">=0.8,<0.9" 925 | attrs = "*" 926 | darglint = ">=1.2,<2.0" 927 | flake8 = ">=3.7,<4.0" 928 | flake8-bandit = ">=2.1,<3.0" 929 | flake8-broken-line = ">=0.3,<0.4" 930 | flake8-bugbear = ">=20.1,<22.0" 931 | flake8-commas = ">=2.0,<3.0" 932 | flake8-comprehensions = ">=3.1,<4.0" 933 | flake8-debugger = ">=4.0,<5.0" 934 | flake8-docstrings = ">=1.3,<2.0" 935 | flake8-eradicate = ">=1.0,<2.0" 936 | flake8-isort = ">=4.0,<5.0" 937 | flake8-quotes = ">=3.0,<4.0" 938 | flake8-rst-docstrings = ">=0.2.3,<0.3.0" 939 | flake8-string-format = ">=0.3,<0.4" 940 | pep8-naming = ">=0.11,<0.12" 941 | pygments = ">=2.4,<3.0" 942 | typing_extensions = ">=3.6,<4.0" 943 | 944 | [[package]] 945 | name = "zipp" 946 | version = "3.6.0" 947 | description = "Backport of pathlib-compatible object wrapper for zip files" 948 | category = "dev" 949 | optional = false 950 | python-versions = ">=3.6" 951 | 952 | [package.extras] 953 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 954 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 955 | 956 | [metadata] 957 | lock-version = "1.1" 958 | python-versions = "^3.8.3" 959 | content-hash = "5cb0921d7dfcb86807942426261936acd4e3f4e0e37a471d7197194090bbd684" 960 | 961 | [metadata.files] 962 | astor = [ 963 | {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, 964 | {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, 965 | ] 966 | atomicwrites = [ 967 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 968 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 969 | ] 970 | attrs = [ 971 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 972 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 973 | ] 974 | bandit = [ 975 | {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, 976 | {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, 977 | ] 978 | black = [ 979 | {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, 980 | {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, 981 | ] 982 | click = [ 983 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 984 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 985 | ] 986 | colorama = [ 987 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 988 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 989 | ] 990 | construct = [ 991 | {file = "construct-2.10.67.tar.gz", hash = "sha256:730235fedf4f2fee5cfadda1d14b83ef1bf23790fb1cc579073e10f70a050883"}, 992 | ] 993 | construct-typing = [ 994 | {file = "construct-typing-0.5.1.tar.gz", hash = "sha256:ed0b2bbbce1d4b31d184886320cec0fee6748f0ae329ceb307d08263a2205266"}, 995 | {file = "construct_typing-0.5.1-py3-none-any.whl", hash = "sha256:c6d6af9a84cb00116d1a205f1b53cf0261748e504a41cc1da269d95dbccaeb44"}, 996 | ] 997 | coverage = [ 998 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 999 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 1000 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 1001 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 1002 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 1003 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 1004 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 1005 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 1006 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 1007 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 1008 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 1009 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 1010 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 1011 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 1012 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 1013 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 1014 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 1015 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 1016 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 1017 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 1018 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 1019 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 1020 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 1021 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 1022 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 1023 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 1024 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 1025 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 1026 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 1027 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 1028 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 1029 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 1030 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 1031 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 1032 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 1033 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 1034 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 1035 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 1036 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 1037 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 1038 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 1039 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 1040 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 1041 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 1042 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 1043 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 1044 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 1045 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 1046 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 1047 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 1048 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 1049 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 1050 | ] 1051 | darglint = [ 1052 | {file = "darglint-1.8.0-py3-none-any.whl", hash = "sha256:ac6797bcc918cd8d8f14c168a4a364f54e1aeb4ced59db58e7e4c6dfec2fe15c"}, 1053 | {file = "darglint-1.8.0.tar.gz", hash = "sha256:aa605ef47817a6d14797d32b390466edab621768ea4ca5cc0f3c54f6d8dcaec8"}, 1054 | ] 1055 | docutils = [ 1056 | {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, 1057 | {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, 1058 | ] 1059 | eradicate = [ 1060 | {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"}, 1061 | ] 1062 | filelock = [ 1063 | {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"}, 1064 | {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"}, 1065 | ] 1066 | flake8 = [ 1067 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 1068 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 1069 | ] 1070 | flake8-bandit = [ 1071 | {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, 1072 | ] 1073 | flake8-broken-line = [ 1074 | {file = "flake8-broken-line-0.3.0.tar.gz", hash = "sha256:f74e052833324a9e5f0055032f7ccc54b23faabafe5a26241c2f977e70b10b50"}, 1075 | {file = "flake8_broken_line-0.3.0-py3-none-any.whl", hash = "sha256:611f79c7f27118e7e5d3dc098ef7681c40aeadf23783700c5dbee840d2baf3af"}, 1076 | ] 1077 | flake8-bugbear = [ 1078 | {file = "flake8-bugbear-21.9.2.tar.gz", hash = "sha256:db9a09893a6c649a197f5350755100bb1dd84f110e60cf532fdfa07e41808ab2"}, 1079 | {file = "flake8_bugbear-21.9.2-py36.py37.py38-none-any.whl", hash = "sha256:4f7eaa6f05b7d7ea4cbbde93f7bcdc5438e79320fa1ec420d860c181af38b769"}, 1080 | ] 1081 | flake8-commas = [ 1082 | {file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"}, 1083 | {file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"}, 1084 | ] 1085 | flake8-comprehensions = [ 1086 | {file = "flake8-comprehensions-3.7.0.tar.gz", hash = "sha256:6b3218b2dde8ac5959c6476cde8f41a79e823c22feb656be2710cd2a3232cef9"}, 1087 | {file = "flake8_comprehensions-3.7.0-py3-none-any.whl", hash = "sha256:a5d7aea6315bbbd6fbcb2b4e80bff6a54d1600155e26236e555d0c6fe1d62522"}, 1088 | ] 1089 | flake8-debugger = [ 1090 | {file = "flake8-debugger-4.0.0.tar.gz", hash = "sha256:e43dc777f7db1481db473210101ec2df2bd39a45b149d7218a618e954177eda6"}, 1091 | {file = "flake8_debugger-4.0.0-py3-none-any.whl", hash = "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032"}, 1092 | ] 1093 | flake8-docstrings = [ 1094 | {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, 1095 | {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, 1096 | ] 1097 | flake8-eradicate = [ 1098 | {file = "flake8-eradicate-1.1.0.tar.gz", hash = "sha256:f5917d6dbca352efcd10c15fdab9c55c48f0f26f6a8d47898b25d39101f170a8"}, 1099 | {file = "flake8_eradicate-1.1.0-py3-none-any.whl", hash = "sha256:d8e39b684a37c257a53cda817d86e2d96c9ba3450ddc292742623a5dfee04d9e"}, 1100 | ] 1101 | flake8-isort = [ 1102 | {file = "flake8-isort-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"}, 1103 | {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"}, 1104 | ] 1105 | flake8-polyfill = [ 1106 | {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, 1107 | {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, 1108 | ] 1109 | flake8-quotes = [ 1110 | {file = "flake8-quotes-3.3.0.tar.gz", hash = "sha256:f1dd87830ed77ff2ce47fc0ee0fd87ae20e8f045355354ffbf4dcaa18d528217"}, 1111 | ] 1112 | flake8-rst-docstrings = [ 1113 | {file = "flake8-rst-docstrings-0.2.3.tar.gz", hash = "sha256:3045794e1c8467fba33aaea5c246b8369efc9c44ef8b0b20199bb6df7a4bd47b"}, 1114 | {file = "flake8_rst_docstrings-0.2.3-py3-none-any.whl", hash = "sha256:565bbb391d7e4d0042924102221e9857ad72929cdd305b26501736ec22c1451a"}, 1115 | ] 1116 | flake8-string-format = [ 1117 | {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, 1118 | {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, 1119 | ] 1120 | ghp-import = [ 1121 | {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, 1122 | {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, 1123 | ] 1124 | gitdb = [ 1125 | {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, 1126 | {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, 1127 | ] 1128 | gitpython = [ 1129 | {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, 1130 | {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, 1131 | ] 1132 | hypothesis = [ 1133 | {file = "hypothesis-6.23.2-py3-none-any.whl", hash = "sha256:ffe81bf1e3122edfcdbf21b31a0b8db3759fac6b87cdc2f9ae32cd360311ccf4"}, 1134 | {file = "hypothesis-6.23.2.tar.gz", hash = "sha256:b71b257916c91484716a10220ed2b9a0cf82acc3ed8ef421bb2aa0a671761053"}, 1135 | ] 1136 | importlib-metadata = [ 1137 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 1138 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 1139 | ] 1140 | isort = [ 1141 | {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, 1142 | {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, 1143 | ] 1144 | jinja2 = [ 1145 | {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, 1146 | {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, 1147 | ] 1148 | markdown = [ 1149 | {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, 1150 | {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, 1151 | ] 1152 | markupsafe = [ 1153 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, 1154 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, 1155 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, 1156 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, 1157 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, 1158 | {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, 1159 | {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, 1160 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 1161 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 1162 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 1163 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 1164 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 1165 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 1166 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, 1167 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, 1168 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, 1169 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 1170 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 1171 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 1172 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 1173 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 1174 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 1175 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 1176 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 1177 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, 1178 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, 1179 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, 1180 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 1181 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 1182 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, 1183 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 1184 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 1185 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 1186 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 1187 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 1188 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 1189 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, 1190 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, 1191 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, 1192 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 1193 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 1194 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 1195 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 1196 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 1197 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 1198 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 1199 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 1200 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 1201 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, 1202 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, 1203 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, 1204 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 1205 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 1206 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 1207 | ] 1208 | mccabe = [ 1209 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 1210 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 1211 | ] 1212 | mergedeep = [ 1213 | {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, 1214 | {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, 1215 | ] 1216 | mkdocs = [ 1217 | {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, 1218 | {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, 1219 | ] 1220 | mkdocs-material = [ 1221 | {file = "mkdocs-material-7.3.3.tar.gz", hash = "sha256:3444b681f47e62c0ec7166bfb6f12360a26c751224cd6d3b3816f7310827073f"}, 1222 | {file = "mkdocs_material-7.3.3-py2.py3-none-any.whl", hash = "sha256:56bcc4e43356b97caa07706b0f446a3d9c39eb9aa1ad64131686d555270fc65e"}, 1223 | ] 1224 | mkdocs-material-extensions = [ 1225 | {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, 1226 | {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, 1227 | ] 1228 | more-itertools = [ 1229 | {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"}, 1230 | {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, 1231 | ] 1232 | mypy = [ 1233 | {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, 1234 | {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, 1235 | {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, 1236 | {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, 1237 | {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, 1238 | {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, 1239 | {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, 1240 | {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, 1241 | {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, 1242 | {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, 1243 | {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, 1244 | {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, 1245 | {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, 1246 | {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, 1247 | {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, 1248 | {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, 1249 | {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, 1250 | {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, 1251 | {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, 1252 | {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, 1253 | {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, 1254 | {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, 1255 | {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, 1256 | ] 1257 | mypy-extensions = [ 1258 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1259 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1260 | ] 1261 | packaging = [ 1262 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 1263 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 1264 | ] 1265 | pathspec = [ 1266 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 1267 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 1268 | ] 1269 | pbr = [ 1270 | {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, 1271 | {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, 1272 | ] 1273 | pep8-naming = [ 1274 | {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"}, 1275 | {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"}, 1276 | ] 1277 | platformdirs = [ 1278 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 1279 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 1280 | ] 1281 | pluggy = [ 1282 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 1283 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 1284 | ] 1285 | py = [ 1286 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 1287 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 1288 | ] 1289 | pycodestyle = [ 1290 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 1291 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 1292 | ] 1293 | pydocstyle = [ 1294 | {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, 1295 | {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, 1296 | ] 1297 | pyflakes = [ 1298 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 1299 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 1300 | ] 1301 | pygments = [ 1302 | {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, 1303 | {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, 1304 | ] 1305 | pymdown-extensions = [ 1306 | {file = "pymdown-extensions-9.0.tar.gz", hash = "sha256:01e4bec7f4b16beaba0087a74496401cf11afd69e3a11fe95cb593e5c698ef40"}, 1307 | {file = "pymdown_extensions-9.0-py3-none-any.whl", hash = "sha256:430cc2fbb30cef2df70edac0b4f62614a6a4d2b06462e32da4ca96098b7c1dfb"}, 1308 | ] 1309 | pyparsing = [ 1310 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 1311 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 1312 | ] 1313 | pytest = [ 1314 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 1315 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 1316 | ] 1317 | pytest-cov = [ 1318 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 1319 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 1320 | ] 1321 | pytest-flake8 = [ 1322 | {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, 1323 | {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, 1324 | ] 1325 | pytest-mypy = [ 1326 | {file = "pytest-mypy-0.8.1.tar.gz", hash = "sha256:1fa55723a4bf1d054fcba1c3bd694215a2a65cc95ab10164f5808afd893f3b11"}, 1327 | {file = "pytest_mypy-0.8.1-py3-none-any.whl", hash = "sha256:6e68e8eb7ceeb7d1c83a1590912f784879f037b51adfb9c17b95c6b2fc57466b"}, 1328 | ] 1329 | python-dateutil = [ 1330 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1331 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1332 | ] 1333 | pyyaml = [ 1334 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 1335 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 1336 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 1337 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 1338 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 1339 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 1340 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 1341 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 1342 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 1343 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 1344 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 1345 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 1346 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 1347 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 1348 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 1349 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 1350 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 1351 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 1352 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 1353 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 1354 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 1355 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 1356 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 1357 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 1358 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 1359 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 1360 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 1361 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 1362 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 1363 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 1364 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 1365 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 1366 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 1367 | ] 1368 | pyyaml-env-tag = [ 1369 | {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, 1370 | {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, 1371 | ] 1372 | regex = [ 1373 | {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:094a905e87a4171508c2a0e10217795f83c636ccc05ddf86e7272c26e14056ae"}, 1374 | {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"}, 1375 | {file = "regex-2021.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b0f2f874c6a157c91708ac352470cb3bef8e8814f5325e3c5c7a0533064c6a24"}, 1376 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"}, 1377 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"}, 1378 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"}, 1379 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"}, 1380 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"}, 1381 | {file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"}, 1382 | {file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"}, 1383 | {file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"}, 1384 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"}, 1385 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"}, 1386 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"}, 1387 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"}, 1388 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"}, 1389 | {file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"}, 1390 | {file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"}, 1391 | {file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"}, 1392 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"}, 1393 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"}, 1394 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"}, 1395 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"}, 1396 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"}, 1397 | {file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"}, 1398 | {file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"}, 1399 | {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19b8f6d23b2dc93e8e1e7e288d3010e58fafed323474cf7f27ab9451635136d9"}, 1400 | {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"}, 1401 | {file = "regex-2021.10.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:951be934dc25d8779d92b530e922de44dda3c82a509cdb5d619f3a0b1491fafa"}, 1402 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"}, 1403 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"}, 1404 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"}, 1405 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"}, 1406 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"}, 1407 | {file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"}, 1408 | {file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"}, 1409 | {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6dcf53d35850ce938b4f044a43b33015ebde292840cef3af2c8eb4c860730fff"}, 1410 | {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"}, 1411 | {file = "regex-2021.10.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2ec1c106d3f754444abf63b31e5c4f9b5d272272a491fa4320475aba9e8157c"}, 1412 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"}, 1413 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"}, 1414 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"}, 1415 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"}, 1416 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"}, 1417 | {file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"}, 1418 | {file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"}, 1419 | {file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"}, 1420 | ] 1421 | restructuredtext-lint = [ 1422 | {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"}, 1423 | ] 1424 | six = [ 1425 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1426 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1427 | ] 1428 | smmap = [ 1429 | {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, 1430 | {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, 1431 | ] 1432 | snowballstemmer = [ 1433 | {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, 1434 | {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, 1435 | ] 1436 | sortedcontainers = [ 1437 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1438 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1439 | ] 1440 | stevedore = [ 1441 | {file = "stevedore-3.4.0-py3-none-any.whl", hash = "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e"}, 1442 | {file = "stevedore-3.4.0.tar.gz", hash = "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1"}, 1443 | ] 1444 | sumtypes = [ 1445 | {file = "sumtypes-0.1a5-py2.py3-none-any.whl", hash = "sha256:148b45ba70b8b82c043e1a9346009b32cf13e03a1083919edf4fd76cb952a8af"}, 1446 | {file = "sumtypes-0.1a5.tar.gz", hash = "sha256:bb57fa40a341fc9204ba7f03f181a5d3c0ab765f4ee517a6ba96d8311f4713e7"}, 1447 | ] 1448 | testfixtures = [ 1449 | {file = "testfixtures-6.18.3-py2.py3-none-any.whl", hash = "sha256:6ddb7f56a123e1a9339f130a200359092bd0a6455e31838d6c477e8729bb7763"}, 1450 | {file = "testfixtures-6.18.3.tar.gz", hash = "sha256:2600100ae96ffd082334b378e355550fef8b4a529a6fa4c34f47130905c7426d"}, 1451 | ] 1452 | toml = [ 1453 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1454 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1455 | ] 1456 | tomli = [ 1457 | {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, 1458 | {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, 1459 | ] 1460 | typing-extensions = [ 1461 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 1462 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 1463 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 1464 | ] 1465 | watchdog = [ 1466 | {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, 1467 | {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, 1468 | {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, 1469 | {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, 1470 | {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, 1471 | {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, 1472 | {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, 1473 | {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, 1474 | {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, 1475 | {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, 1476 | {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, 1477 | {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, 1478 | {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, 1479 | {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, 1480 | {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, 1481 | {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, 1482 | {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, 1483 | {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, 1484 | {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, 1485 | {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, 1486 | {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, 1487 | {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, 1488 | {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, 1489 | ] 1490 | wcwidth = [ 1491 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 1492 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 1493 | ] 1494 | wemake-python-styleguide = [ 1495 | {file = "wemake-python-styleguide-0.15.3.tar.gz", hash = "sha256:8b89aedabae67b7b915908ed06c178b702068137c0d8afe1fb59cdc829cd2143"}, 1496 | {file = "wemake_python_styleguide-0.15.3-py3-none-any.whl", hash = "sha256:a382f6c9ec87d56daa08a11e47cab019c99b384f1393b32564ebc74c6da80441"}, 1497 | ] 1498 | zipp = [ 1499 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 1500 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 1501 | ] 1502 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "borsh-construct" 3 | version = "0.1.0" 4 | description = "Python implementation of Borsh serialization, built on the Construct library." 5 | authors = ["kevinheavey "] 6 | license = "MIT" 7 | homepage = "https://github.com/near/borsh-construct-py" 8 | repository = "https://github.com/near/borsh-construct-py" 9 | readme = "README.md" 10 | 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.8.3" 14 | construct-typing = "^0.5.1" 15 | sumtypes = "^0.1a5" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "^5.2" 19 | wemake-python-styleguide = "^0.15.3" 20 | mypy = "^0.910" 21 | black = "^21.7b0" 22 | pydocstyle = "^6.1.1" 23 | isort = "^5.9.3" 24 | coverage = {extras = ["toml"], version = "^5.5"} 25 | pytest-cov = "^2.12.1" 26 | mkdocs = "^1.2.2" 27 | mkdocs-material = "^7.2.6" 28 | flake8 = "^3.9.2" 29 | pytest-flake8 = "^1.0.7" 30 | pytest-mypy = "^0.8.1" 31 | hypothesis = "^6.23.0" 32 | 33 | [build-system] 34 | requires = ["poetry-core>=1.0.0"] 35 | build-backend = "poetry.core.masonry.api" 36 | 37 | [tool.coverage.paths] 38 | source = ["src", "*/site-packages"] 39 | 40 | [tool.coverage.run] 41 | branch = true 42 | source = ["borsh_construct"] 43 | 44 | [tool.coverage.report] 45 | show_missing = true 46 | fail_under = 100 47 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov --doctest-glob="*.md" --flake8 --mypy 3 | -------------------------------------------------------------------------------- /src/borsh_construct/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version, PackageNotFoundError 2 | from construct import Flag as Bool 3 | from construct import Int8sl as I8 4 | from construct import Int16sl as I16 5 | from construct import Int16ul as U16 6 | from construct import Int32sl as I32 7 | from construct import Int64sl as I64 8 | from construct import Int64ul as U64 9 | 10 | from .core import ( 11 | F32, 12 | F64, 13 | I128, 14 | U8, 15 | U32, 16 | U128, 17 | Vec, 18 | CStruct, 19 | TupleStruct, 20 | Bytes, 21 | String, 22 | Option, 23 | HashMap, 24 | HashSet, 25 | ) 26 | from .enum import Enum 27 | 28 | try: 29 | __version__ = version(__name__) 30 | except PackageNotFoundError: # pragma: no cover 31 | __version__ = "unknown" 32 | 33 | __all__ = [ 34 | "I8", 35 | "I16", 36 | "I32", 37 | "U8", 38 | "I64", 39 | "I128", 40 | "U16", 41 | "U32", 42 | "U64", 43 | "U128", 44 | "F32", 45 | "F64", 46 | "Bool", 47 | "Vec", 48 | "CStruct", 49 | "TupleStruct", 50 | "Bytes", 51 | "String", 52 | "Enum", 53 | "Option", 54 | "HashMap", 55 | "HashSet", 56 | ] 57 | -------------------------------------------------------------------------------- /src/borsh_construct/core.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List, Tuple 2 | from math import isnan 3 | from construct import Adapter, Array, BytesInteger, Construct 4 | from construct import singleton # type: ignore 5 | from construct import FormatField, FormatFieldError, GreedyBytes, IfThenElse 6 | from construct import Int8ul as U8 7 | from construct import Int32ul as U32 8 | from construct import Pass, Prefixed, PrefixedArray 9 | from construct import Sequence 10 | from construct import Struct 11 | 12 | TUPLE_DATA = "tuple_data" 13 | 14 | NAMED_TUPLE_FIELD_ERROR = ValueError("TupleStruct cannot have named fields") 15 | UNNAMED_SUBCON_ERROR = ValueError("CStruct fields and enum variants must be named") 16 | NON_STR_NAME_ERROR = ValueError("Names must be strings.") 17 | TUPLE_DATA_NAME_ERROR = ValueError( 18 | f"The name {TUPLE_DATA} is reserved. If you encountered this " 19 | "error it's either a wild coincidence or you're " 20 | "doing it wrong." # noqa: C812 21 | ) 22 | UNDERSCORE_NAME_ERROR = ValueError("names cannot start with an underscore.") 23 | U128 = BytesInteger(16, signed=False, swapped=True) 24 | I128 = BytesInteger(16, signed=True, swapped=True) 25 | 26 | 27 | class TupleStruct(Sequence): 28 | """Python implementation of Rust tuple struct.""" 29 | 30 | def __init__(self, *subcons) -> None: 31 | super().__init__(*subcons) # type: ignore 32 | for subcon in self.subcons: 33 | if subcon.name is not None: 34 | raise NAMED_TUPLE_FIELD_ERROR 35 | 36 | 37 | class CStruct(Struct): 38 | """Python implementation of Rust C-like struct.""" 39 | 40 | def __init__(self, *subcons) -> None: 41 | super().__init__(*subcons) 42 | for subcon in subcons: 43 | check_subcon_name(subcon.name) 44 | 45 | 46 | def _check_name_not_null(name: Optional[str]) -> None: 47 | if name is None: 48 | raise UNNAMED_SUBCON_ERROR 49 | 50 | 51 | def check_subcon_name(name: Optional[str]) -> None: 52 | """Check that CStructs and Enums have valid names.""" # noqa: DAR101 53 | _check_name_not_null(name) 54 | if not isinstance(name, str): 55 | raise NON_STR_NAME_ERROR 56 | if name == TUPLE_DATA: 57 | raise TUPLE_DATA_NAME_ERROR 58 | if name[0] == "_": 59 | raise UNDERSCORE_NAME_ERROR 60 | 61 | 62 | class FormatFieldNoNan(FormatField): 63 | """Adapted form of `construct.FormatField` that forbids nan.""" 64 | 65 | def _parse(self, stream, context, path): 66 | result = super()._parse(stream, context, path) 67 | if isnan(result): 68 | raise FormatFieldError("Borsh does not support nan.") 69 | return result 70 | 71 | def _build(self, obj, stream, context, path): 72 | if isnan(obj): 73 | raise FormatFieldError("Borsh does not support nan.") 74 | return super()._build(obj, stream, context, path) 75 | 76 | 77 | @singleton 78 | def F32() -> FormatFieldNoNan: # noqa: N802 79 | """Little endian, 32-bit IEEE floating point number.""" 80 | return FormatFieldNoNan("<", "f") 81 | 82 | 83 | @singleton 84 | def F64() -> FormatFieldNoNan: # noqa: N802 85 | """Little endian, 64-bit IEEE floating point number.""" 86 | return FormatFieldNoNan("<", "d") 87 | 88 | 89 | def Vec(subcon: Construct) -> Array: # noqa: N802 90 | """Dynamic sized array. 91 | 92 | Args: 93 | subcon (Construct): the type of the array members. 94 | 95 | Returns: 96 | Array: a Construct PrefixedArray. 97 | """ 98 | return PrefixedArray(U32, subcon) 99 | 100 | 101 | Bytes = Prefixed(U32, GreedyBytes) 102 | 103 | 104 | class _String(Adapter): 105 | def __init__(self) -> None: 106 | super().__init__(Bytes) # type: ignore 107 | 108 | def _decode(self, obj: bytes, context, path) -> str: 109 | return obj.decode("utf8") 110 | 111 | def _encode(self, obj: str, context, path) -> bytes: 112 | return bytes(obj, "utf8") 113 | 114 | 115 | String = _String() 116 | 117 | 118 | class Option(Adapter): 119 | """Borsh implementation for Rust's Option type.""" 120 | 121 | _discriminator_key = "discriminator" 122 | _value_key = "value" 123 | 124 | def __init__(self, subcon: Construct) -> None: 125 | option_struct = CStruct( 126 | self._discriminator_key / U8, 127 | self._value_key 128 | / IfThenElse(lambda this: this[self._discriminator_key] == 0, Pass, subcon), 129 | ) 130 | super().__init__(option_struct) # type: ignore 131 | 132 | def _decode(self, obj, context, path) -> Any: 133 | return obj[self._value_key] 134 | 135 | def _encode(self, obj, context, path) -> dict: 136 | discriminator = 0 if obj is None else 1 137 | return {self._discriminator_key: discriminator, self._value_key: obj} 138 | 139 | 140 | class HashMap(Adapter): 141 | """Borsh implementation for Rust HashMap.""" 142 | 143 | def __init__(self, key_subcon: Construct, value_subcon: Construct) -> None: 144 | super().__init__( 145 | PrefixedArray(U32, TupleStruct(key_subcon, value_subcon)), 146 | ) # type: ignore 147 | 148 | def _decode(self, obj: List[Tuple[Any, Any]], context, path) -> dict: 149 | return dict(obj) 150 | 151 | def _encode(self, obj, context, path) -> List[Tuple]: 152 | return sorted(obj.items()) 153 | 154 | 155 | class HashSet(Adapter): 156 | """Python implementation of Rust HashSet.""" 157 | 158 | def __init__(self, subcon: Construct) -> None: 159 | super().__init__(PrefixedArray(U32, subcon)) # type: ignore 160 | 161 | def _decode(self, obj, context, path) -> set: 162 | return set(obj) 163 | 164 | def _encode(self, obj, context, path) -> list: 165 | return sorted(obj) 166 | -------------------------------------------------------------------------------- /src/borsh_construct/enum.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Tuple, Union, cast, Any, Dict 3 | from sumtypes import sumtype, constructor 4 | from construct import Pass, Renamed, Adapter, Switch, Container, Construct 5 | import attr 6 | 7 | from .core import CStruct, TupleStruct, U8, TUPLE_DATA, check_subcon_name 8 | 9 | 10 | def _rust_enum(klass): 11 | indexed = sumtype(klass) 12 | for idx, cname in enumerate(indexed._sumtype_constructor_names): # noqa: WPS437 13 | constructr = getattr(indexed, cname) 14 | constructr.index = idx 15 | 16 | # __getitem__ magic method cannot be classmethod 17 | @classmethod 18 | def getitem(cls, _index: int): # noqa: WPS614 19 | return getattr(cls, cls._sumtype_constructor_names[_index]) 20 | 21 | indexed.getitem = getitem 22 | 23 | return indexed 24 | 25 | 26 | def _tuple_struct(): 27 | return constructor(**{TUPLE_DATA: attr.ib(type=tuple)}) 28 | 29 | 30 | def _unit_struct(): 31 | return constructor() 32 | 33 | 34 | def _clike_struct(*fields: str): 35 | return constructor(*fields) 36 | 37 | 38 | def _handle_cstruct_variant(underlying_variant, variant_name) -> Tuple[str, Any]: 39 | subcon_names: List[str] = [] 40 | for s in underlying_variant.subcons: 41 | name = s.name 42 | subcon_names.append(cast(str, name)) 43 | return variant_name, _clike_struct(*subcon_names) 44 | 45 | 46 | def _handle_struct_variant(variant) -> Tuple[str, Any]: 47 | variant_name = variant.name 48 | check_subcon_name(variant_name) 49 | underlying_variant = variant.subcon if isinstance(variant, Renamed) else variant 50 | if isinstance(underlying_variant, TupleStruct): 51 | return variant_name, _tuple_struct() 52 | elif isinstance(underlying_variant, CStruct): 53 | return _handle_cstruct_variant(underlying_variant, variant_name) 54 | variant_type = type(underlying_variant) 55 | raise ValueError(f"Unrecognized variant type: {variant_type}") 56 | 57 | 58 | def _make_cls_dict(*variants) -> dict: # noqa: WPS210 59 | result = {"__doc__": "Python representation of Rust's Enum type."} 60 | seen_variant_names = set() 61 | for variant in variants: 62 | if isinstance(variant, str): 63 | variant_name = variant 64 | result[variant] = _unit_struct() 65 | else: 66 | variant_name = variant.name 67 | key, val = _handle_struct_variant(variant) 68 | result[key] = val 69 | if variant_name in seen_variant_names: 70 | raise ValueError("Enum variant names must be unique.") 71 | seen_variant_names.add(variant_name) 72 | return result 73 | 74 | 75 | def _make_enum(*variants, name: str): 76 | klass = type(name, (object,), _make_cls_dict(*variants)) 77 | return _rust_enum(klass) 78 | 79 | 80 | class Enum(Adapter): 81 | """Borsh representation of Rust's enum type.""" 82 | 83 | _index_key = "index" 84 | _value_key = "value" 85 | 86 | def __init__(self, *variants: Union[str, Construct], enum_name: str) -> None: 87 | """Init enum. 88 | 89 | Note: unlike other types, you must use the `enum_name` keyword argument 90 | to give your Enum a name when instantiating it. 91 | """ # noqa: DAR101 92 | switch_cases = {} 93 | for idx, var in enumerate(variants): 94 | if isinstance(var, str): 95 | parser = Pass 96 | else: 97 | parser = var 98 | switch_cases[idx] = parser 99 | enum_struct = CStruct( 100 | self._index_key / U8, 101 | self._value_key / Switch(lambda this: this.index, switch_cases), 102 | ) 103 | super().__init__(enum_struct) # type: ignore 104 | self.variants = variants 105 | self.enum_name = enum_name 106 | self.enum = _make_enum(*variants, name=enum_name) 107 | 108 | def _decode(self, obj: Any, context, path) -> Any: 109 | index = obj.index 110 | enum_variant = self.enum.getitem(index) 111 | val = obj.value 112 | if val is None: 113 | return enum_variant() 114 | if isinstance(val, Container): 115 | return enum_variant(**{k: v for k, v in val.items() if k != "_io"}) 116 | return enum_variant(val) 117 | 118 | def _encode(self, obj: Any, context, path) -> Dict[str, Any]: 119 | index = obj.index 120 | as_dict = attr.asdict(obj) 121 | if as_dict: 122 | try: 123 | to_build = as_dict[TUPLE_DATA] 124 | except KeyError: 125 | to_build = as_dict 126 | else: 127 | to_build = None 128 | return {self._index_key: index, self._value_key: to_build} 129 | -------------------------------------------------------------------------------- /src/borsh_construct/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/borsh-construct-py/e49dee71716ec217e8c9966aaa621c61669f7c15/src/borsh_construct/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def pytest_configure(config): 5 | """Flake8 is very verbose by default. Silence it.""" # noqa: DAR101 6 | logging.getLogger("flake8").setLevel(logging.ERROR) 7 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | """Core tests.""" 2 | from typing import Any 3 | 4 | import pytest 5 | from borsh_construct import ( 6 | F32, 7 | F64, 8 | I8, 9 | I16, 10 | I32, 11 | I64, 12 | I128, 13 | U8, 14 | U16, 15 | U32, 16 | U64, 17 | U128, 18 | Bool, 19 | Vec, 20 | CStruct, 21 | TupleStruct, 22 | Enum, 23 | String, 24 | Option, 25 | HashMap, 26 | HashSet, 27 | Bytes, 28 | ) 29 | from borsh_construct.core import ( 30 | NAMED_TUPLE_FIELD_ERROR, 31 | TUPLE_DATA, 32 | UNNAMED_SUBCON_ERROR, 33 | NON_STR_NAME_ERROR, 34 | UNDERSCORE_NAME_ERROR, 35 | TUPLE_DATA_NAME_ERROR, 36 | ) 37 | from construct import Construct, Float32l, Float64l, FormatField, FormatFieldError 38 | 39 | ENUM = Enum( 40 | "Unit", 41 | "TupleVariant" / TupleStruct(U128, String, I64, Option(U16)), 42 | "CStructVariant" 43 | / CStruct("u128_field" / U128, "string_field" / String, "vec_field" / Vec(U16)), 44 | enum_name="Placeholder", 45 | ) 46 | 47 | TYPE_INPUT_EXPECTED = ( 48 | (Bool, True, [1]), 49 | (Bool, False, [0]), 50 | (U8, 255, [255]), 51 | (I8, -128, [128]), 52 | (U16, 65535, [255, 255]), 53 | (I16, -32768, [0, 128]), 54 | (U32, 4294967295, [255, 255, 255, 255]), 55 | (I32, -2147483648, [0, 0, 0, 128]), 56 | (U64, 18446744073709551615, [255, 255, 255, 255, 255, 255, 255, 255]), 57 | (I64, -9223372036854775808, [0, 0, 0, 0, 0, 0, 0, 128]), 58 | ( 59 | U128, 60 | 340282366920938463463374607431768211455, 61 | [ 62 | 255, 63 | 255, 64 | 255, 65 | 255, 66 | 255, 67 | 255, 68 | 255, 69 | 255, 70 | 255, 71 | 255, 72 | 255, 73 | 255, 74 | 255, 75 | 255, 76 | 255, 77 | 255, 78 | ], 79 | ), 80 | ( 81 | I128, 82 | -170141183460469231731687303715884105728, 83 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128], 84 | ), 85 | (F32, 0.5, [0, 0, 0, 63]), 86 | (F64, -0.5, [0, 0, 0, 0, 0, 0, 224, 191]), 87 | (I16[3], [1, 2, 3], [1, 0, 2, 0, 3, 0]), 88 | (Vec(I16), [1, 1], [2, 0, 0, 0, 1, 0, 1, 0]), 89 | ( 90 | TupleStruct(U128, String, I64, Option(U16)), 91 | [123, "hello", 1400, 13], 92 | [ 93 | 123, 94 | 0, 95 | 0, 96 | 0, 97 | 0, 98 | 0, 99 | 0, 100 | 0, 101 | 0, 102 | 0, 103 | 0, 104 | 0, 105 | 0, 106 | 0, 107 | 0, 108 | 0, 109 | 5, 110 | 0, 111 | 0, 112 | 0, 113 | 104, 114 | 101, 115 | 108, 116 | 108, 117 | 111, 118 | 120, 119 | 5, 120 | 0, 121 | 0, 122 | 0, 123 | 0, 124 | 0, 125 | 0, 126 | 1, 127 | 13, 128 | 0, 129 | ], 130 | ), 131 | ( 132 | CStruct("u128_field" / U128, "string_field" / String, "vec_field" / Vec(U16)), 133 | {"u128_field": 1033, "string_field": "hello", "vec_field": [1, 2, 3]}, 134 | [ 135 | 9, 136 | 4, 137 | 0, 138 | 0, 139 | 0, 140 | 0, 141 | 0, 142 | 0, 143 | 0, 144 | 0, 145 | 0, 146 | 0, 147 | 0, 148 | 0, 149 | 0, 150 | 0, 151 | 5, 152 | 0, 153 | 0, 154 | 0, 155 | 104, 156 | 101, 157 | 108, 158 | 108, 159 | 111, 160 | 3, 161 | 0, 162 | 0, 163 | 0, 164 | 1, 165 | 0, 166 | 2, 167 | 0, 168 | 3, 169 | 0, 170 | ], 171 | ), 172 | (ENUM, ENUM.enum.Unit(), [0]), 173 | ( 174 | ENUM, 175 | ENUM.enum.TupleVariant([10, "hello", 13, 12]), 176 | [ 177 | 1, 178 | 10, 179 | 0, 180 | 0, 181 | 0, 182 | 0, 183 | 0, 184 | 0, 185 | 0, 186 | 0, 187 | 0, 188 | 0, 189 | 0, 190 | 0, 191 | 0, 192 | 0, 193 | 0, 194 | 5, 195 | 0, 196 | 0, 197 | 0, 198 | 104, 199 | 101, 200 | 108, 201 | 108, 202 | 111, 203 | 13, 204 | 0, 205 | 0, 206 | 0, 207 | 0, 208 | 0, 209 | 0, 210 | 0, 211 | 1, 212 | 12, 213 | 0, 214 | ], 215 | ), 216 | ( 217 | ENUM, 218 | ENUM.enum.CStructVariant( 219 | u128_field=15, 220 | string_field="hi", 221 | vec_field=[3, 2, 1], 222 | ), 223 | [ 224 | 2, 225 | 15, 226 | 0, 227 | 0, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 0, 233 | 0, 234 | 0, 235 | 0, 236 | 0, 237 | 0, 238 | 0, 239 | 0, 240 | 0, 241 | 2, 242 | 0, 243 | 0, 244 | 0, 245 | 104, 246 | 105, 247 | 3, 248 | 0, 249 | 0, 250 | 0, 251 | 3, 252 | 0, 253 | 2, 254 | 0, 255 | 1, 256 | 0, 257 | ], 258 | ), 259 | ( 260 | HashMap(U8, ENUM), 261 | {2: ENUM.enum.Unit(), 1: ENUM.enum.TupleVariant([11, "hello", 123, None])}, 262 | [ 263 | 2, 264 | 0, 265 | 0, 266 | 0, 267 | 1, 268 | 1, 269 | 11, 270 | 0, 271 | 0, 272 | 0, 273 | 0, 274 | 0, 275 | 0, 276 | 0, 277 | 0, 278 | 0, 279 | 0, 280 | 0, 281 | 0, 282 | 0, 283 | 0, 284 | 0, 285 | 5, 286 | 0, 287 | 0, 288 | 0, 289 | 104, 290 | 101, 291 | 108, 292 | 108, 293 | 111, 294 | 123, 295 | 0, 296 | 0, 297 | 0, 298 | 0, 299 | 0, 300 | 0, 301 | 0, 302 | 0, 303 | 2, 304 | 0, 305 | ], 306 | ), 307 | (HashSet(U8), {1, 2, 3}, [3, 0, 0, 0, 1, 2, 3]), 308 | (Bytes, b"\x01\x02\x03", [3, 0, 0, 0, 1, 2, 3]), 309 | ( 310 | String, 311 | "🚀🚀🚀", 312 | [12, 0, 0, 0, 240, 159, 154, 128, 240, 159, 154, 128, 240, 159, 154, 128], 313 | ), 314 | ) 315 | 316 | 317 | @pytest.mark.parametrize("obj_type,obj_input,expected", TYPE_INPUT_EXPECTED) 318 | def test_serde(obj_type: Construct, obj_input: Any, expected: Any) -> None: 319 | """Tests that inputs are serialized and deserialized as expected.""" 320 | serialized = obj_type.build(obj_input) 321 | assert list(serialized) == expected 322 | deserialized = obj_type.parse(serialized) 323 | assert deserialized == obj_input 324 | 325 | 326 | @pytest.mark.parametrize( 327 | "nonan_type,construct_type", 328 | [(F32, Float32l), (F64, Float64l)], 329 | ) 330 | def test_nan_floats(nonan_type: FormatField, construct_type: FormatField) -> None: 331 | """Check that error is raised if you try to build or parse nan floats.""" 332 | nan = float("nan") # noqa: WPS456 333 | with pytest.raises(FormatFieldError): 334 | nonan_type.build(nan) 335 | nan_serialized = construct_type.build(nan) 336 | with pytest.raises(FormatFieldError): 337 | nonan_type.parse(nan_serialized) 338 | 339 | 340 | def test_named_tuple_struct_field_raises() -> None: 341 | """Check that error is raised if TupleStruct field is named.""" 342 | with pytest.raises(ValueError) as exc: 343 | TupleStruct("foo" / U8) 344 | assert exc.value == NAMED_TUPLE_FIELD_ERROR 345 | 346 | 347 | def test_unnamed_subcon_raises() -> None: 348 | """Check that error is raised when enum variant or CStruct field is unnamed.""" 349 | with pytest.raises(ValueError) as excinfo: 350 | Enum("foo", TupleStruct(U8), enum_name="placeholder") 351 | assert str(excinfo.value) == str(UNNAMED_SUBCON_ERROR) 352 | 353 | 354 | def test_non_str_name_raises() -> None: 355 | """Check that error is raised when subcon name is not a string.""" 356 | with pytest.raises(ValueError) as excinfo: 357 | CStruct(1 / U8) # type: ignore 358 | assert str(excinfo.value) == str(NON_STR_NAME_ERROR) 359 | 360 | 361 | def test_tuple_data_name_raises() -> None: 362 | """Check that error is raised when subcon name is not a string.""" 363 | with pytest.raises(ValueError) as excinfo: 364 | CStruct(TUPLE_DATA / U8) 365 | assert str(excinfo.value) == str(TUPLE_DATA_NAME_ERROR) 366 | 367 | 368 | def test_underscore_name_raises() -> None: 369 | """Check that error is raised when subcon name starts with underscore.""" 370 | with pytest.raises(ValueError) as excinfo: 371 | CStruct("_foo" / U8) 372 | assert str(excinfo.value) == str(UNDERSCORE_NAME_ERROR) 373 | 374 | 375 | def test_unrecognized_variant_type_raises() -> None: 376 | """Check that error is raised if variant type is not valid.""" 377 | with pytest.raises(ValueError) as excinfo: 378 | Enum("foo" / U8, enum_name="placeholder") 379 | assert "Unrecognized" in str(excinfo.value) 380 | 381 | 382 | def test_duplicate_variant_name_raises() -> None: 383 | """Check error raised if two variants in same Enum have same name.""" 384 | with pytest.raises(ValueError) as excinfo: 385 | Enum("foo", "foo", enum_name="placeholder") 386 | assert "must be unique" in str(excinfo.value) 387 | -------------------------------------------------------------------------------- /tests/test_hypothesis.py: -------------------------------------------------------------------------------- 1 | from hypothesis import given 2 | import hypothesis.strategies as st 3 | 4 | from borsh_construct import ( 5 | U8, 6 | I8, 7 | U16, 8 | I16, 9 | U32, 10 | I32, 11 | U64, 12 | I64, 13 | U128, 14 | I128, 15 | F32, 16 | F64, 17 | String, 18 | Bytes, 19 | HashMap, 20 | HashSet, 21 | Vec, 22 | Option, 23 | ) 24 | 25 | 26 | borsh_simple_types = ( 27 | U8, 28 | I8, 29 | U16, 30 | I16, 31 | U32, 32 | I32, 33 | U64, 34 | I64, 35 | U128, 36 | I128, 37 | F32, 38 | F64, 39 | Bytes, 40 | String, 41 | ) 42 | borsh_compound_types = ( 43 | HashMap, 44 | HashSet, 45 | Vec, 46 | Option, 47 | ) 48 | 49 | 50 | @given(st.text()) 51 | def test_string(s): 52 | """Test string encoding/decoding.""" 53 | assert String.parse(String.build(s)) == s 54 | 55 | 56 | u8_ints = st.integers(0, 255) 57 | i8_ints = st.integers(-128, 127) 58 | u16_ints = st.integers(0, 65535) 59 | i16_ints = st.integers(-32768, 32767) 60 | u32_ints = st.integers(0, 4294967295) 61 | i32_ints = st.integers(-2147483648, 2147483647) 62 | u64_ints = st.integers(0, 18446744073709551615) 63 | i64_ints = st.integers(-9223372036854775808, 9223372036854775807) 64 | u128_ints = st.integers(0, 340282366920938463463374607431768211455) 65 | i128_ints = st.integers( 66 | -170141183460469231731687303715884105728, 67 | 170141183460469231731687303715884105727, 68 | ) 69 | f32_floats = st.floats(width=32, allow_nan=False) 70 | f64_floats = st.floats(width=64, allow_nan=False) 71 | numeric_strategies = ( 72 | u8_ints, 73 | i8_ints, 74 | u16_ints, 75 | i16_ints, 76 | u32_ints, 77 | i32_ints, 78 | u64_ints, 79 | i64_ints, 80 | u128_ints, 81 | i128_ints, 82 | ) 83 | 84 | type_map = { 85 | U8: u8_ints, 86 | I8: i8_ints, 87 | U16: u16_ints, 88 | I16: i16_ints, 89 | U32: u32_ints, 90 | I32: i32_ints, 91 | U64: u64_ints, 92 | I64: i64_ints, 93 | U128: u128_ints, 94 | I128: i128_ints, 95 | F32: f32_floats, 96 | F64: f64_floats, 97 | Bytes: st.binary(), 98 | String: st.text(), 99 | } 100 | 101 | 102 | @given(u8_ints) 103 | def test_u8(s): 104 | """Test U8 encoding/decoding.""" 105 | assert U8.parse(U8.build(s)) == s 106 | 107 | 108 | @given(i8_ints) 109 | def test_i8(s): 110 | """Test I8 encoding/decoding.""" 111 | assert I8.parse(I8.build(s)) == s 112 | 113 | 114 | @given(u16_ints) 115 | def test_u16(s): 116 | """Test I16 encoding/decoding.""" 117 | assert U16.parse(U16.build(s)) == s 118 | 119 | 120 | @given(i16_ints) 121 | def test_i16(s): 122 | """Test I16 encoding/decoding.""" 123 | assert I16.parse(I16.build(s)) == s 124 | 125 | 126 | @given(u32_ints) 127 | def test_u32(s): 128 | """Test U32 encoding/decoding.""" 129 | assert U32.parse(U32.build(s)) == s 130 | 131 | 132 | @given(i32_ints) 133 | def test_i32(s): 134 | """Test I32 encoding/decoding.""" 135 | assert I32.parse(I32.build(s)) == s 136 | 137 | 138 | @given(u64_ints) 139 | def test_u64(s): 140 | """Test U64 encoding/decoding.""" 141 | assert U64.parse(U64.build(s)) == s 142 | 143 | 144 | @given(i64_ints) 145 | def test_i64(s): 146 | """Test I64 encoding/decoding.""" 147 | assert I64.parse(I64.build(s)) == s 148 | 149 | 150 | @given(u128_ints) 151 | def test_u128(s): 152 | """Test U128 encoding/decoding.""" 153 | assert U128.parse(U128.build(s)) == s 154 | 155 | 156 | @given(i128_ints) 157 | def test_i128(s): 158 | """Test I128 encoding/decoding.""" 159 | assert I128.parse(I128.build(s)) == s 160 | 161 | 162 | @given(f32_floats) 163 | def test_f32(s): 164 | """Test F32 encoding/decoding.""" 165 | assert F32.parse(F32.build(s)) == s 166 | 167 | 168 | @given(f64_floats) 169 | def test_f64(s): 170 | """Test F64 encoding/decoding.""" 171 | assert F64.parse(F64.build(s)) == s 172 | 173 | 174 | @st.composite 175 | def element_and_borsh_type(draw, elements=st.sampled_from(borsh_simple_types)): 176 | """Return a simple borsh type and a simple example of data fitting that type.""" 177 | borsh_type = draw(elements) 178 | data_strategy = type_map[borsh_type] 179 | data = draw(data_strategy) 180 | return data, borsh_type 181 | 182 | 183 | @st.composite 184 | def list_data_and_borsh_type( 185 | draw, 186 | elements=st.sampled_from(borsh_simple_types), 187 | min_length=0, 188 | unique=False, 189 | ): 190 | """Return a list whose elements fit a particular borsh type.""" 191 | borsh_type = draw(elements) 192 | data_strategy = type_map[borsh_type] 193 | data = draw(st.lists(data_strategy, min_size=min_length, unique=unique)) 194 | return data, borsh_type, len(data) 195 | 196 | 197 | @given(list_data_and_borsh_type()) # type: ignore 198 | def test_vec(data_borsh_type): 199 | """Test Vec encoding.""" 200 | data, borsh_type, _ = data_borsh_type 201 | vec_type = Vec(borsh_type) 202 | assert vec_type.parse(vec_type.build(data)) == data 203 | 204 | 205 | @given(list_data_and_borsh_type(min_length=1)) # type: ignore 206 | def test_array(data_borsh_type): 207 | """Test Array encoding.""" 208 | data, borsh_type, length = data_borsh_type 209 | array_type = borsh_type[length] 210 | assert array_type.parse(array_type.build(data)) == data 211 | 212 | 213 | @given(element_and_borsh_type()) # type: ignore 214 | def test_option(data_borsh_type): 215 | """Test Option encoding.""" 216 | data, borsh_type = data_borsh_type 217 | option_type = Option(borsh_type) 218 | assert option_type.parse(option_type.build(data)) == data 219 | 220 | 221 | @given(list_data_and_borsh_type()) # type: ignore 222 | def test_hashset(data_borsh_type): 223 | """Test HashSet encoding.""" 224 | data, borsh_type, _ = data_borsh_type 225 | data = set(data) 226 | set_type = HashSet(borsh_type) 227 | assert set_type.parse(set_type.build(data)) == data 228 | --------------------------------------------------------------------------------