Repository: whitemech/hoa-utils (language: python)

Issue:
Refactoring

Problem found on the grammar definition
I was trying to parse the aut3.2.hoa automaton included in tests/examples, but I found inconsistencies in the grammar definition of label expressions.

In particular, the first transition label is `[!0 & !1]` has to be parsed as the conjunction of two negated atoms. Instead, it's parsed as the negation of a conjunction of one atom and a negated atom, which is wrong. The same happens for the third transition label `[!0 & 1]`.

I think the problem is a matter of operator precedence in the grammar (i.e. the precedence of negation wrt conjunction).

For reproducibility, below you can find a test snippet:

```
    def test_hoa_body(self):
        """Test that the HOA body is correct."""
        state_edges_dict = OrderedDict({})
        state_edges_dict[State(0)] = [Edge([0],
                                           label=AndLabelExpression([NotLabelExpression(AtomLabelExpression('0')),
                                                                     NotLabelExpression(AtomLabelExpression('1'))])),
                                      Edge([0],
                                           label=AndLabelExpression([AtomLabelExpression('0'),
                                                                     NotLabelExpression(AtomLabelExpression('1'))]),
                                           acc_sig=frozenset({0})),
                                      Edge([0],
                                           label=AndLabelExpression([NotLabelExpression(AtomLabelExpression('0')),
                                                                     AtomLabelExpression('1')]),
                                           acc_sig=frozenset({1})),
                                      Edge([0],
                                           label=AndLabelExpression([AtomLabelExpression('0'),
                                                                     AtomLabelExpression('1')]),
                                           acc_sig=frozenset({0, 1}))]
        hoa_body = HOABody(state_edges_dict)
        assert self.hoa_body == hoa_body
```

Thanks!

finish tests on parsing for all examples
This PR aims at:

- fixing some small bugs related to string parsing
- finishing all tests for all automata in tests/examples folder


Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..f5faf4a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,26 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+### Subject of the issue
+Describe your issue here.
+
+### Your environment
+- OS: [e.g. iOS]
+- Python version: [e.g. 3.7.2]
+- Package Version [e.g. 0.1.2]
+- Anything else you consider helpful.
+
+### Steps to reproduce
+Tell us how to reproduce this issue.
+
+### Expected behaviour
+Tell us what should happen
+
+### Actual behaviour
+Tell us what happens instead
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..da0506c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,29 @@
+## Proposed changes
+
+Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
+
+## Fixes
+
+If it fixes a bug or resolves a feature request, be sure to link to that issue.
+
+## Types of changes
+
+What types of changes does your code introduce?
+_Put an `x` in the boxes that apply_
+
+- [ ] Bugfix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+
+## Checklist
+
+_Put an `x` in the boxes that apply._
+
+- [ ] I have read the [CONTRIBUTING](../blob/master/CONTRIBUTING.md) doc
+- [ ] I am making a pull request against the `develop` branch (left side). Also you should start your branch off our `develop`.
+- [ ] Lint and unit tests pass locally with my changes
+- [ ] I have added tests that prove my fix is effective or that my feature works
+
+## Further comments
+
+If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...
diff --git a/.github/RELEASE_PR.md b/.github/RELEASE_PR.md
new file mode 100644
index 0000000..46cacb9
--- /dev/null
+++ b/.github/RELEASE_PR.md
@@ -0,0 +1,30 @@
+## Release summary
+
+Version number: [e.g. 1.0.1]
+
+## Release details
+
+Describe in short the main changes with the new release.
+
+## Checklist
+
+_Put an `x` in the boxes that apply._
+
+- [ ] I have read the [CONTRIBUTING](../master/CONTRIBUTING.rst) doc
+- [ ] I am making a pull request against the `master` branch (left side), from `develop`
+- [ ] I've updated the dependencies versions in `poetry.lock` to the latest, wherever is possible.
+- [ ] Lint and unit tests pass locally (please run tests also manually, not only with `tox`)
+- [ ] I built the documentation and updated it with the latest changes
+- [ ] I've added an item in `HISTORY.md` for this release
+- [ ] I bumped the version number in the `__version__.py` file.
+- [ ] I published the latest version on TestPyPI and checked that the following command work:
+       ```pip install hoa-utils==<version-number> --index-url https://test.pypi.org/simple --force --no-cache-dir --no-deps```
+- [ ] After merging the PR, I'll publish the build also on PyPI. Then, I'll make sure the following
+      command will work:
+      ```pip install hoa-utils==<version_number> --force --no-cache-dir --no-deps```  
+- [ ] After merging the PR, I'll tag the repo with `v${VERSION_NUMVER}` (e.g. `v0.1.2`)
+
+
+## Further comments
+
+Write here any other comment about the release, if any.
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..955dc30
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,29 @@
+name: docs
+
+on:
+  push:
+    branches:
+      - develop
+      - master
+  pull_request:
+
+jobs:
+  run:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        python-version: [3.7]
+
+    timeout-minutes: 30
+
+    steps:
+    - uses: actions/checkout@master
+    - uses: actions/setup-python@master
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run:  pip install tox
+    - name: Generate Documentation
+      run: tox -e docs
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..470be65
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,43 @@
+name: lint
+
+on:
+  push:
+    branches:
+      - develop
+      - master
+  pull_request:
+
+jobs:
+  run:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        python-version: [3.7]
+
+    timeout-minutes: 30
+
+    steps:
+    - uses: actions/checkout@master
+    - uses: actions/setup-python@master
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run:  pip install tox
+    # - name: Pylint
+    #   run: tox -e pylint
+    - name: Code style check
+      run: |
+        tox -e black-check
+        tox -e isort-check
+        tox -e flake8
+        tox -e vulture
+    - name: Static type check
+      run: tox -e mypy
+    - name: Check copyright
+      run: tox -e check-copyright
+    - name: Misc checks
+      run: |
+        tox -e bandit
+        tox -e safety
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..379ed43
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,8 @@
+[MASTER]
+ignore-patterns=
+
+[MESSAGES CONTROL]
+disable=
+
+[IMPORTS]
+ignored-modules=
diff --git a/AUTHORS.md b/AUTHORS.md
new file mode 100644
index 0000000..658fc22
--- /dev/null
+++ b/AUTHORS.md
@@ -0,0 +1,10 @@
+# Credits
+
+## Maintainer
+
+* [Marco Favorito](https://github.com/marcofavorito) <[marco.favorito@gmail.com](mailto:marco.favorito@gmail.com)>
+* [Francesco Fuggitti](https://github.com/francescofuggitti) <[francesco.fuggitti@gmail.com](mailto:francesco.fuggitti@gmail.com)>
+
+## Contributors
+
+None yet. [Why not be the first](./contributing.md)?
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..d23f477
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,10 @@
+# Contributing
+
+Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
+
+If you need support, want to report/fix a bug, ask for/implement features, you can check the
+[Issues page](https://github.com/whitemech/hoa-utils/issues)
+or [submit a Pull request](https://github.com/whitemech/hoa-utils/pulls).
+
+For other kinds of feedback, you can contact one of the
+[authors](./authors.md) by email.
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 0000000..a35162f
--- /dev/null
+++ b/HISTORY.md
@@ -0,0 +1,5 @@
+# History
+
+## 0.1.0 (2020-08-29)
+
+* First release on PyPI.
diff --git a/LICENSE b/LICENSE
index b5fe995..65c5ca8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,165 @@
-MIT License
-
-Copyright (c) 2019 Temporal Logics @ Sapienza
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..1e6701c
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,17 @@
+include AUTHORS.md
+include CONTRIBUTING.md
+include HISTORY.md
+include LICENSE
+include README.md
+include pyproject.toml
+include poetry.lock
+include mkdocs.yml
+include Makefile
+
+recursive-include hoa *
+recursive-include tests *
+
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
+
+recursive-include docs *.md Makefile *.jpg *.png *.gif
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..aa20acb
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,130 @@
+.PHONY: clean clean-test clean-pyc clean-build docs help
+.DEFAULT_GOAL := help
+
+define BROWSER_PYSCRIPT
+import os, webbrowser, sys
+
+try:
+	from urllib import pathname2url
+except:
+	from urllib.request import pathname2url
+
+webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
+endef
+export BROWSER_PYSCRIPT
+
+define PRINT_HELP_PYSCRIPT
+import re, sys
+
+for line in sys.stdin:
+	match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
+	if match:
+		target, help = match.groups()
+		print("%-20s %s" % (target, help))
+endef
+export PRINT_HELP_PYSCRIPT
+
+BROWSER := python -c "$$BROWSER_PYSCRIPT"
+
+help:
+	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
+
+clean: clean-build clean-pyc clean-test clean-docs ## remove all build, test, coverage and Python artifacts
+
+clean-build: ## remove build artifacts
+	rm -fr build/
+	rm -fr dist/
+	rm -fr .eggs/
+	find . -name '*.egg-info' -exec rm -fr {} +
+	find . -name '*.egg' -exec rm -f {} +
+
+clean-pyc: ## remove Python file artifacts
+	find . -name '*.pyc' -exec rm -f {} +
+	find . -name '*.pyo' -exec rm -f {} +
+	find . -name '*~' -exec rm -f {} +
+	find . -name '__pycache__' -exec rm -fr {} +
+
+clean-docs:  ## remove MkDocs products.
+	mkdocs build --clean
+	rm -fr site/
+
+
+clean-test: ## remove test and coverage artifacts
+	rm -fr .tox/
+	rm -f .coverage
+	rm -fr htmlcov/
+	rm -fr .pytest_cache
+	rm -fr .mypy_cache
+	rm -fr coverage.xml
+
+lint-all: black isort lint static bandit safety vulture pylint ## run all linters
+
+lint: ## check style with flake8
+	flake8 hoa tests scripts
+
+static: ## static type checking with mypy
+	mypy hoa tests scripts
+
+isort: ## sort import statements with isort
+	isort hoa tests scripts
+
+isort-check: ## check import statements order with isort
+	isort --check-only hoa tests scripts
+
+black: ## apply black formatting
+	black hoa tests scripts
+
+black-check: ## check black formatting
+	black --check --verbose hoa tests scripts
+
+bandit: ## run bandit
+	bandit hoa tests scripts
+
+safety: ## run safety
+	safety
+
+pylint: ## run pylint
+	pylint hoa tests scripts
+
+vulture: ## run vulture
+	vulture hoa scripts/whitelist.py
+
+test: ## run tests quickly with the default Python
+	pytest tests --doctest-modules \
+        hoa tests/ \
+        --cov=hoa \
+        --cov-report=xml \
+        --cov-report=html \
+        --cov-report=term
+
+test-all: ## run tests on every Python version with tox
+	tox
+
+coverage: ## check code coverage quickly with the default Python
+	coverage run --source hoa -m pytest
+	coverage report -m
+	coverage html
+	$(BROWSER) htmlcov/index.html
+
+docs: ## generate MkDocs HTML documentation, including API docs
+	mkdocs build --clean
+	$(BROWSER) site/index.html
+
+servedocs: docs ## compile the docs watching for changes
+	mkdocs build --clean
+	python -c 'print("###### Starting local server. Press Control+C to stop server ######")'
+	mkdocs serve
+
+release: dist ## package and upload a release
+	twine upload dist/*
+
+dist: clean ## builds source and wheel package
+	python setup.py sdist
+	python setup.py bdist_wheel
+	ls -l dist
+
+install: clean ## install the package to the active Python's site-packages
+	python setup.py install
+
+develop: clean ## install the package in development mode
+	pip install -e .
diff --git a/README.md b/README.md
index a8d3693..ff135ad 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,119 @@
-# hoa2dot
-HOA format to Graphviz DOT translator (and vice versa)
+<h1 align="center">
+  <b>HOA utils</b>
+</h1>
+
+<p align="center">
+  <a href="https://pypi.org/project/hoa-utils">
+    <img alt="PyPI" src="https://img.shields.io/pypi/v/hoa-utils">
+  </a>
+  <a href="https://pypi.org/project/hoa-utils">
+    <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/hoa-utils" />
+  </a>
+  <a href="">
+    <img alt="PyPI - Status" src="https://img.shields.io/pypi/status/hoa-utils" />
+  </a>
+  <a href="">
+    <img alt="PyPI - Implementation" src="https://img.shields.io/pypi/implementation/hoa-utils" />
+  </a>
+  <a href="">
+    <img alt="PyPI - Wheel" src="https://img.shields.io/pypi/wheel/hoa-utils" />
+  </a>
+  <a href="https://github.com/whitemech/hoa-utils/blob/master/LICENSE">
+    <img alt="GitHub" src="https://img.shields.io/github/license/whitemech/hoa-utils" />
+  </a>
+</p>
+<p align="center">
+  <a href="">
+    <img alt="test" src="https://github.com/whitemech/hoa-utils/workflows/test/badge.svg">
+  </a>
+  <a href="">
+    <img alt="lint" src="https://github.com/whitemech/hoa-utils/workflows/lint/badge.svg">
+  </a>
+  <a href="">
+    <img alt="docs" src="https://github.com/whitemech/hoa-utils/workflows/docs/badge.svg">
+  </a>
+  <a href="https://codecov.io/gh/whitemech/hoa-utils">
+    <img src="https://codecov.io/gh/whitemech/hoa-utils/branch/master/graph/badge.svg" />
+  </a>
+</p>
+<p align="center">
+  <a href="https://img.shields.io/badge/flake8-checked-blueviolet">
+    <img alt="" src="https://img.shields.io/badge/flake8-checked-blueviolet">
+  </a>
+  <a href="https://img.shields.io/badge/mypy-checked-blue">
+    <img alt="" src="https://img.shields.io/badge/mypy-checked-blue">
+  </a>
+  <a href="https://img.shields.io/badge/isort-checked-yellow">
+    <img alt="" src="https://img.shields.io/badge/isort-checked-yellow">
+  </a>
+  <a href="https://img.shields.io/badge/code%20style-black-black">
+    <img alt="black" src="https://img.shields.io/badge/code%20style-black-black" />
+  </a>
+  <a href="https://www.mkdocs.org/">
+    <img alt="" src="https://img.shields.io/badge/docs-mkdocs-9cf">
+  </a>
+</p>
+
+Utilities for the HOA format.
+
+## Install
+
+The best way is to install the package from PyPI:
+```
+pip install hoa-utils
+```
+
+Alternatively, you can install it from source (master branch):
+```
+pip install git+https://github.com/whitemech/hoa-utils.git
+```
+
+## What you'll find
+
+- APIs to create and manipulate HOA objects
+- CLI tools to about the HOA format.
+
+The implementation may not be very stable at the moment.
+
+Currently, the only supported CLI tool is:
+- `pyhoafparser`: parse and validate a file in HOA format. 
+
+
+## Development
+
+If you want to contribute, here's how to set up your development environment.
+
+- Install [Poetry](https://python-poetry.org/)
+- Clone the repository: `git clone https://github.com/whitemech/hoa-utils.git && cd hoa-utils`
+- Install the dependencies: `poetry install`
+
+## Tests
+
+To run tests: `tox`
+
+To run only the code tests: `tox -e py3.7`
+
+To run only the code style checks:
+ - `tox -e black-check`
+ - `tox -e isort-check`
+ - `tox -e flake8`
+ 
+ In `tox.ini` you can find all the test environment supported.
+
+## Docs
+
+To build the docs: `mkdocs build`
+
+To view documentation in a browser: `mkdocs serve`
+and then go to [http://localhost:8000](http://localhost:8000)
+
+## Authors
+
+- [Marco Favorito](https://marcofavorito.github.io/)
+- [Francesco Fuggitti](https://francescofuggitti.github.io/)
+
+## License
+
+`hoa-utils` is released under the MIT License.
+
+Copyright 2020 WhiteMech
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..b55a0c3
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,4 @@
+coverage:
+  range: 70..100
+  round: down
+  precision: 2
diff --git a/docs/authors.md b/docs/authors.md
new file mode 100644
index 0000000..02e330e
--- /dev/null
+++ b/docs/authors.md
@@ -0,0 +1,1 @@
+{!../AUTHORS.md!}
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..568877b
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,1 @@
+{!../CONTRIBUTING.md!}
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..7d211af
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,1 @@
+{!../README.md!}
diff --git a/hoa/__init__.py b/hoa/__init__.py
new file mode 100644
index 0000000..54b2f42
--- /dev/null
+++ b/hoa/__init__.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+"""Top-level package for hoa."""
+
+from .__version__ import (
+    __author__,
+    __author_email__,
+    __copyright__,
+    __description__,
+    __license__,
+    __title__,
+    __url__,
+    __version__,
+)
diff --git a/hoa/__version__.py b/hoa/__version__.py
new file mode 100644
index 0000000..ca9436c
--- /dev/null
+++ b/hoa/__version__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""Declaration of metadata for the package."""
+
+__title__ = "hoa"
+__description__ = "Utilities for the HOA format."
+__url__ = "https://github.com/whitemech/hoa-utils.git"
+__version__ = "0.1.0"
+__author__ = "Marco Favorito, Francesco Fuggitti"
+__author_email__ = "marco.favorito@gmail.com, francesco.fuggitti@gmail.com"
+__license__ = "MIT License"
+__copyright__ = "2020 Whitemech"
diff --git a/hoa/ast/__init__.py b/hoa/ast/__init__.py
new file mode 100644
index 0000000..2797f5b
--- /dev/null
+++ b/hoa/ast/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+"""This package contains utilities for the AST of an HOA."""
diff --git a/hoa/ast/acceptance.py b/hoa/ast/acceptance.py
new file mode 100644
index 0000000..3cb6349
--- /dev/null
+++ b/hoa/ast/acceptance.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+"""This module contains the definitions of acceptance atoms."""
+from dataclasses import dataclass
+from enum import Enum
+from functools import reduce, singledispatch
+from typing import Optional, Set, Tuple, Union
+
+from hoa.ast.boolean_expression import (
+    BinaryOp,
+    boolean_op_wrapper,
+    FalseFormula,
+    PositiveAnd,
+    PositiveOr,
+    TrueFormula,
+    UnaryOp,
+)
+from hoa.types import ACCEPTANCE_PARAMETER, identifier
+
+
+class AtomType(Enum):
+    """This is an enumeration to represent the possible atom types."""
+
+    FINITE = "Fin"
+    INFINITE = "Inf"
+
+    def __str__(self):
+        """Get the string representation."""
+        return self.value
+
+
+@boolean_op_wrapper(
+    and_=PositiveAnd["AcceptanceCondition"],
+    or_=PositiveOr["AcceptanceCondition"],
+    not_=None,
+)
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class AcceptanceAtom:
+    """Implement the acceptance atom."""
+
+    atom_type: AtomType
+    acceptance_set: int
+    negated: bool
+
+    def __invert__(self):
+        """Negate the atom."""
+        return AcceptanceAtom(self.atom_type, self.acceptance_set, not self.negated)
+
+
+def Fin(acceptance_set: int):
+    """Return the acceptance atom with finite acceptance."""
+    return AcceptanceAtom(AtomType.FINITE, acceptance_set, False)
+
+
+def NotFin(acceptance_set: int):
+    """
+    Return the acceptance atom with finite acceptance negated'.
+
+    >>> not_fin0 = NotFin(0)
+    >>> fin0 = Fin(0)
+    >>> not_fin0 == ~fin0
+    True
+    """
+    return ~Fin(acceptance_set)
+
+
+def Inf(acceptance_set: int):
+    """Return the acceptance atom with infinite acceptance."""
+    return AcceptanceAtom(AtomType.INFINITE, acceptance_set, False)
+
+
+def NotInf(acceptance_set: int):
+    """
+    Return the acceptance atom with infinite acceptance negated.
+
+    >>> not_inf0 = NotInf(0)
+    >>> inf0 = Inf(0)
+    >>> not_inf0 == ~inf0
+    True
+    """
+    return ~Inf(acceptance_set)
+
+
+AcceptanceCondition = Union[
+    PositiveAnd["AcceptanceCondition"],
+    PositiveOr["AcceptanceCondition"],
+    AcceptanceAtom,
+    FalseFormula,
+    TrueFormula,
+]
+
+
+@singledispatch
+def accepting_sets(_) -> Set[int]:
+    """
+    Compute the accepting sets of an acceptance condition.
+
+    :param _: the acceptance condition formula.
+    :return: the set of accepting sets.
+    """
+    return set()
+
+
+@accepting_sets.register  # type: ignore
+def _(acceptance_condition: BinaryOp):
+    return reduce(
+        lambda x, y: x.union(y), map(accepting_sets, acceptance_condition.operands)
+    )
+
+
+@accepting_sets.register  # type: ignore
+def _(acceptance_condition: UnaryOp):
+    return accepting_sets(acceptance_condition.argument)
+
+
+@accepting_sets.register  # type: ignore
+def _(acceptance_condition: AcceptanceAtom):
+    return {acceptance_condition.acceptance_set}
+
+
+def nb_accepting_sets(acceptance_condition: AcceptanceCondition):
+    """Get the number of accepting sets."""
+    return len(accepting_sets(acceptance_condition))
+
+
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class Acceptance:
+    """This class represents the acceptance in the HOA format."""
+
+    condition: AcceptanceCondition
+    name: Optional[identifier] = None
+    parameters: Tuple[ACCEPTANCE_PARAMETER, ...] = tuple()
diff --git a/hoa/ast/boolean_expression.py b/hoa/ast/boolean_expression.py
new file mode 100644
index 0000000..5ec3773
--- /dev/null
+++ b/hoa/ast/boolean_expression.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the implementation of generic boolean expressions."""
+
+from dataclasses import dataclass
+from typing import Any, ClassVar, Generic, Optional, Sequence, Type, TypeVar
+
+T = TypeVar("T")
+
+
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class BinaryOp(Generic[T]):
+    """Binary operator."""
+
+    SYMBOL: ClassVar[str]
+    operands: Sequence[T]
+
+    def __str__(self) -> str:
+        """Get the string representation."""
+        return f"({self.SYMBOL} {' '.join(map(str, self.operands))})"
+
+    def __repr__(self) -> str:
+        """Get an unambiguous string representation."""
+        return f"{type(self).__name__}({repr(self.operands)})"
+
+
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class UnaryOp(Generic[T]):
+    """Unary operator."""
+
+    SYMBOL: ClassVar[str]
+    argument: T
+
+    def __str__(self) -> str:
+        """Get the string representation."""
+        return f"({self.SYMBOL} {self.argument})"
+
+    def __repr__(self) -> str:
+        """Get an unambiguous string representation."""
+        return f"{type(self).__name__}({repr(self.argument)})"
+
+
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class TrueFormula:
+    """A tautology."""
+
+    def __str__(self) -> str:
+        """Get the string representation."""
+        return "(true)"
+
+    def __repr__(self) -> str:
+        """Get an unambiguous string representation."""
+        return "TrueFormula()"
+
+    def __neg__(self) -> "FalseFormula":
+        """Negate."""
+        return FALSE
+
+
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class FalseFormula:
+    """A contradiction."""
+
+    def __str__(self) -> str:
+        """Get the string representation."""
+        return "(false)"
+
+    def __repr__(self) -> str:
+        """Get an unambiguous string representation."""
+        return "FalseFormula()"
+
+    def __neg__(self) -> "TrueFormula":
+        """Negate."""
+        return TRUE
+
+
+TRUE = TrueFormula()
+FALSE = FalseFormula()
+
+
+class MonotoneOp(type):
+    """Metaclass to simplify monotone operator instantiations."""
+
+    _absorbing: ClassVar[Optional[Any]] = None
+
+    def __call__(cls, *args, **kwargs):
+        """Init the subclass object."""
+        operands = _simplify_monotone_op_operands(cls, *args)
+        if len(operands) == 1:
+            return operands[0]
+
+        return super(MonotoneOp, cls).__call__(operands, **kwargs)
+
+
+class _And(BinaryOp, Generic[T], metaclass=MonotoneOp):
+    """And operator."""
+
+    _absorbing = FALSE
+    SYMBOL = "&"
+
+
+class _Or(BinaryOp, Generic[T], metaclass=MonotoneOp):
+    """Or operator."""
+
+    _absorbing = TRUE
+    SYMBOL = "|"
+
+
+class _PositiveAnd(BinaryOp, Generic[T], metaclass=MonotoneOp):
+    """And operator."""
+
+    _absorbing = FALSE
+    SYMBOL = "&"
+
+
+class _PositiveOr(BinaryOp, Generic[T], metaclass=MonotoneOp):
+    """Or operator."""
+
+    _absorbing = TRUE
+    SYMBOL = "|"
+
+
+class _Not(UnaryOp, Generic[T]):
+    """Not operator."""
+
+    SYMBOL = "!"
+
+
+# _cls should never be specified by keyword, so start it with an
+# underscore.  The presence of _cls is used to detect if this
+# decorator is being called with parameters or not.
+def boolean_op_wrapper(
+    _cls=None,
+    *,
+    and_: Optional[Type[BinaryOp]] = _And,
+    or_: Optional[Type[BinaryOp]] = _Or,
+    not_: Optional[Type[UnaryOp]] = _Not,
+):
+    """
+    Provide the atom class with __and__, __or__ and __not__ dunder methods.
+
+    :param _cls: the class.
+    :param and_: the class for the __and__ operation
+    :param or_: the class for the __or__ operation
+    :param not_: the class for the __not__ operation
+    :return: the same class, endowed with new dunder methods.
+    """
+
+    def _process_class(cls, and_cls, or_cls, not_cls):
+        if and_cls is not None:
+            cls.__and__ = lambda self, other: and_cls(self, other)
+        if or_cls is not None:
+            cls.__or__ = lambda self, other: or_cls(self, other)
+        if not_cls is not None:
+            cls.__invert__ = lambda self: not_cls(self)
+        return cls
+
+    def wrap(cls):
+        return _process_class(cls, and_, or_, not_)
+
+    if _cls is None:
+        return wrap
+
+    return wrap(_cls)
+
+
+And = boolean_op_wrapper(_cls=_And)
+Or = boolean_op_wrapper(_cls=_Or)
+Not = boolean_op_wrapper(_cls=_Not)
+
+PositiveAnd = boolean_op_wrapper(_cls=_PositiveAnd, and_=_PositiveAnd, or_=_PositiveOr)
+PositiveOr = boolean_op_wrapper(_cls=_PositiveOr, and_=_PositiveAnd, or_=_PositiveOr)
+
+
+def _simplify_monotone_op_operands(cls, *operands):
+    operands = list(dict.fromkeys(operands))
+    if len(operands) == 0:
+        return (~cls._absorbing,)
+    elif len(operands) == 1:
+        return (operands[0],)
+    elif cls._absorbing in operands:
+        return cls._absorbing
+
+    # shift-up subformulas with same operator. DFS on expression tree.
+    new_operands = []
+    stack = operands[::-1]  # it is reversed in order to preserve order.
+    while len(stack) > 0:
+        element = stack.pop()
+        if not isinstance(element, cls):
+            new_operands.append(element)
+            continue
+        stack.extend(reversed(element.operands))  # see above regarding reversed.
+
+    return tuple(new_operands)
diff --git a/hoa/ast/label.py b/hoa/ast/label.py
new file mode 100644
index 0000000..0f7d813
--- /dev/null
+++ b/hoa/ast/label.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+"""This module contains the definitions of acceptance atoms."""
+from dataclasses import dataclass
+from functools import reduce, singledispatch
+from typing import Set, Union
+
+from hoa.ast.boolean_expression import (
+    And,
+    BinaryOp,
+    boolean_op_wrapper,
+    FalseFormula,
+    Not,
+    Or,
+    TrueFormula,
+    UnaryOp,
+)
+from hoa.types import alias as alias_type
+
+
+@boolean_op_wrapper(
+    and_=And["LabelExpression"], or_=Or["LabelExpression"], not_=Not["LabelExpression"]
+)
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class LabelAtom:
+    """Implement the label atom."""
+
+    proposition: int
+
+
+@boolean_op_wrapper(
+    and_=And["LabelExpression"], or_=Or["LabelExpression"], not_=Not["LabelExpression"]
+)
+@dataclass(order=True, unsafe_hash=True, frozen=True)
+class LabelAlias:
+    """Implement the label alias."""
+
+    alias: alias_type
+    expression: "LabelExpression"
+
+
+LabelExpression = Union[
+    And["LabelExpression"],
+    Or["LabelExpression"],
+    Not["LabelExpression"],
+    LabelAtom,
+    LabelAlias,
+    FalseFormula,
+    TrueFormula,
+]
+
+
+@singledispatch
+def propositions(_) -> Set[int]:
+    """
+    Compute the accepting sets of an acceptance condition.
+
+    :param _: the acceptance condition formula.
+    :return: the set of accepting sets.
+    """
+    return set()
+
+
+@propositions.register  # type: ignore
+def _(label_expression: BinaryOp):
+    return reduce(lambda x, y: x.union(y), map(propositions, label_expression.operands))
+
+
+@propositions.register  # type: ignore
+def _(label_expression: UnaryOp):
+    return propositions(label_expression.argument)
+
+
+@propositions.register  # type: ignore
+def _(label_expression: LabelAtom):
+    return {label_expression.proposition}
+
+
+@propositions.register  # type: ignore
+def _(label_expression: LabelAlias):
+    return propositions(label_expression.expression)
diff --git a/hoa/core.py b/hoa/core.py
new file mode 100644
index 0000000..3b1c151
--- /dev/null
+++ b/hoa/core.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the core definitions for the tool."""
+from dataclasses import dataclass
+from typing import AbstractSet, Dict, FrozenSet, Optional, Sequence, Tuple, Union
+
+from hoa.ast.acceptance import Acceptance
+from hoa.ast.label import LabelAlias, LabelExpression
+from hoa.types import HEADER_VALUES, headername, identifier, string
+
+
+@dataclass(frozen=True, order=True, unsafe_hash=True)
+class State:
+    """This class represents a state of the automaton."""
+
+    index: int
+    label: Optional[LabelExpression] = None
+    name: Optional[string] = None
+    acc_sig: Optional[FrozenSet[int]] = None
+
+
+@dataclass(frozen=True, order=True)
+class Edge:
+    """This class represents an edge in the automaton."""
+
+    state_conj: Sequence[int]
+    label: Optional[LabelExpression] = None
+    acc_sig: Optional[AbstractSet[int]] = None
+
+
+@dataclass(frozen=True)
+class HOAHeader:
+    """This class implements a data structure for the HOA file format header."""
+
+    format_version: identifier
+    acceptance: Acceptance
+    nb_states: Optional[int] = None
+    start_states: Optional[AbstractSet[AbstractSet[int]]] = None
+    propositions: Optional[Tuple[string, ...]] = None
+    aliases: Optional[Sequence[LabelAlias]] = None
+    tool: Optional[Union[string, Sequence[string]]] = None
+    name: Optional[string] = None
+    properties: Optional[Sequence[identifier]] = None
+    headernames: Optional[Dict[headername, Sequence[HEADER_VALUES]]] = None
+
+
+@dataclass(frozen=True)
+class HOABody:
+    """This class implements a data structure for the HOA file format header."""
+
+    state2edges: Dict[State, Sequence[Edge]]
+
+
+@dataclass(frozen=True)
+class HOA:
+    """This class implements a data structure for the HOA file format."""
+
+    header: HOAHeader
+    body: HOABody
diff --git a/hoa/dumpers.py b/hoa/dumpers.py
new file mode 100644
index 0000000..1a1d3f5
--- /dev/null
+++ b/hoa/dumpers.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains utilities to dump the HOA objects."""
+from functools import singledispatch
+from typing import TextIO
+
+from hoa.ast.acceptance import nb_accepting_sets
+from hoa.core import Edge, HOA, HOABody, HOAHeader, State
+from hoa.printers import acceptance_condition_to_string, label_expression_to_string
+from hoa.types import acceptance_parameter, hoa_header_value
+
+
+def dump(hoa: HOA, fp: TextIO) -> None:
+    """
+    Dump the data to a file.
+
+    :param hoa: the HOA object.
+    :param fp: the file pointer.
+    :return: None.
+    """
+    fp.write(dumps(hoa))
+
+
+@singledispatch
+def dumps(obj):
+    """
+    Dump the data into a string in HOA format.
+
+    :param obj: the HOA object.
+    :return: the string in HOA format.
+    """
+    raise ValueError(f"Type {type(obj)} not expected.")
+
+
+@dumps.register  # type: ignore
+def _(hoa: HOA) -> str:
+    """
+    Dump the data into a string in HOA format.
+
+    :param hoa: the HOA object.
+    :return: the string in HOA format.
+    """
+    header = dumps(hoa.header)
+    body = dumps(hoa.body)
+    return header + "--BODY--\n" + body + "--END--"
+
+
+@dumps.register  # type: ignore
+def _(hoa_header: HOAHeader):
+    """
+    Dump the data into a string in HOA format.
+
+    :param hoa_header: the HOA header.
+    :return: the string in HOA format.
+    """
+    s = "HOA: {}\n".format(hoa_header.format_version)
+    if hoa_header.nb_states is not None:
+        s += "States: {}\n".format(hoa_header.nb_states)
+    if hoa_header.start_states is not None:
+        s += (
+            "\n".join(
+                "Start: {}".format(" & ".join(map(str, start_state_set)))
+                for start_state_set in hoa_header.start_states
+            )
+            + "\n"
+        )
+    if hoa_header.propositions is not None and len(hoa_header.propositions) > 0:
+        propositions_string = '"' + '" "'.join(p for p in hoa_header.propositions) + '"'
+        s += "AP: {nb} {prop}\n".format(
+            nb=len(hoa_header.propositions), prop=propositions_string
+        )
+    if hoa_header.aliases is not None and len(hoa_header.aliases) > 0:
+        s += (
+            "\n".join(
+                [
+                    "Alias: {} {}".format(
+                        alias_label.alias,
+                        label_expression_to_string(alias_label.expression),
+                    )
+                    for alias_label in hoa_header.aliases
+                ]
+            )
+            + "\n"
+        )
+    acceptance_str = acceptance_condition_to_string(hoa_header.acceptance.condition)
+    nb_accepting_sets_ = nb_accepting_sets(hoa_header.acceptance.condition)
+    s += f"Acceptance: {nb_accepting_sets_} {acceptance_str}\n"
+    if hoa_header.acceptance.name is not None:
+        s += "acc-name: {} {}\n".format(
+            hoa_header.acceptance.name,
+            " ".join(
+                map(
+                    acceptance_parameter.to_acceptance_parameter,
+                    hoa_header.acceptance.parameters,
+                )
+            ),
+        )
+    if hoa_header.tool is not None:
+        s += "tool: {}\n".format(" ".join(hoa_header.tool))
+    if hoa_header.name is not None:
+        s += 'name: "{}"\n'.format(hoa_header.name)
+    if hoa_header.properties is not None and len(hoa_header.properties) > 0:
+        s += "properties: {}\n".format(" ".join(hoa_header.properties))
+    if hoa_header.headernames is not None and len(hoa_header.headernames) > 0:
+        s += (
+            "\n".join(
+                [
+                    "{}: {}".format(
+                        key, " ".join(map(hoa_header_value.to_hoa_header_value, values))
+                    )
+                    for key, values in hoa_header.headernames.items()
+                ]
+            )
+            + "\n"
+        )
+
+    return s
+
+
+@dumps.register  # type: ignore
+def _(hoa_body: HOABody):
+    """
+    Dump the data into a string in HOA format.
+
+    :param hoa_body: the HOA body.
+    :return: the string in HOA format.
+    """
+    return (
+        "\n".join(
+            [
+                dumps(state) + "\n" + "\n".join(map(dumps, edges))
+                for state, edges in hoa_body.state2edges.items()
+            ]
+        )
+        + "\n"
+    )
+
+
+@dumps.register  # type: ignore
+def _(state: State) -> str:
+    """Get the HOA format representation of the state."""
+    s = "State: "
+    if state.label is not None:
+        s += "[" + label_expression_to_string(state.label) + "]" + " "
+    s += str(state.index) + " "
+    if state.name is not None:
+        s += '"' + state.name + '"' + " "
+    if state.acc_sig is not None:
+        s += "{" + " ".join(map(str, state.acc_sig)) + "}"
+    return s
+
+
+@dumps.register  # type: ignore
+def _(edge: Edge) -> str:
+    """Get the HOA format representation of the edge."""
+    s = ""
+    if edge.label is not None:
+        s += "[" + label_expression_to_string(edge.label) + "]" + " "
+    s += "&".join(map(str, edge.state_conj)) + " "
+    if edge.acc_sig is not None:
+        s += "{" + " ".join(map(str, edge.acc_sig)) + "}"
+    return s
diff --git a/hoa/grammars/hoa.lark b/hoa/grammars/hoa.lark
new file mode 100644
index 0000000..5427ed7
--- /dev/null
+++ b/hoa/grammars/hoa.lark
@@ -0,0 +1,74 @@
+start: automaton
+automaton : header "--BODY--" body "--END--"
+
+header : format_version header_item*
+format_version : "HOA:" IDENTIFIER
+header_item : num_states
+            | start_state
+            | propositions
+            | alias
+            | acceptance
+            | acc_name
+            | tool
+            | name
+            | properties
+            | headername
+num_states: "States:" INT
+start_state: "Start:" state_conj
+propositions: "AP:" INT STRING*
+alias: "Alias:" ANAME label_expr
+acceptance: "Acceptance:" INT acceptance_cond
+acc_name: "acc-name:" IDENTIFIER (IDENTIFIER|INT)*
+tool: "tool:" STRING STRING?
+name: "name:" STRING
+properties: "properties:" IDENTIFIER*
+headername: HEADERNAME (BOOLEAN|INT|STRING|IDENTIFIER)*
+
+body: (state_name edge*)*
+// the optional dstring can be used to name the state for
+// cosmetic or debugging purposes, as in ltl2dstar's format
+state_name: "State:" label? INT STRING? acc_sig?
+acc_sig: "{" INT* "}"
+edge: label? state_conj acc_sig?
+label:  "[" label_expr "]"
+
+
+state_conj : INT ("&" INT)*
+label_expr : "(" label_expr ")"
+           | or_label_expr
+           | and_label_expr
+           | not_label_expr
+           | atom_label_expr
+           | boolean_label_expr
+           | alias_label_expr
+
+or_label_expr: label_expr "|" label_expr
+and_label_expr: label_expr "&" label_expr
+not_label_expr: "!" label_expr
+alias_label_expr: ANAME
+atom_label_expr: INT
+boolean_label_expr: BOOLEAN
+
+acceptance_cond : "(" acceptance_cond ")"
+                | or_acceptance_cond
+                | and_acceptance_cond
+                | not_acceptance_cond
+                | boolean_acceptance_cond
+                | atom_acceptance_cond
+or_acceptance_cond.2 : acceptance_cond "|" acceptance_cond
+and_acceptance_cond.1 : acceptance_cond "&" acceptance_cond
+not_acceptance_cond : IDENTIFIER "(" "!" INT ")"
+atom_acceptance_cond : IDENTIFIER "(" INT ")"
+boolean_acceptance_cond : BOOLEAN
+
+STRING: /"(\\.|[^\\"])*"/                // a C-like double-quoted string
+INT: /0|[1-9][0-9]*/                     // A non-negative integer less than 2^31 written in base 10 (with no useless 0 at the beginning).
+COMMENT: /\/\*.*\*\//                    // Comments may be introduced between any token by enclosing them with /* and */ (with proper nesting, i.e. /*a/*b*/c*/ is one comment). C++-style comments are not considered because they require newlines. Tools can use comments to output additional information (e.g. debugging data) that should be discarded upon reading.
+WHITESPACE: /[ \t\n\r]/                  // Except in double-quoted strings and comments, whitespace is used only for tokenization and can be discarded afterwards.
+BOOLEAN: /[tf]/                          // The true and false Boolean constants.
+IDENTIFIER: /[a-zA-Z_][0-9a-zA-Z_-]*/    // An identifier made of letters, digits, - and _. Digits and - may not by used as first character, and t or f are not valid identifiers.
+ANAME: /@[0-9a-zA-Z_-]+/                 // An alias name, i.e., "@" followed by some alphanumeric characters, - or _. These are used to identify atomic propositions or subformulas.
+HEADERNAME: /[a-zA-Z_][0-9a-zA-Z_-]*:"/  // Header names are similar to identifiers, except that they are immediately followed by a colon (i.e. no comment or space allowed). If an IDENTIFIER or a BOOLEAN is immediately followed by a colon, it should be considered as a HEADERNAME.
+
+%ignore WHITESPACE
+%ignore COMMENT
diff --git a/hoa/helpers/__init__.py b/hoa/helpers/__init__.py
new file mode 100644
index 0000000..4fe292a
--- /dev/null
+++ b/hoa/helpers/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+"""This package contain helper functions."""
diff --git a/hoa/helpers/base.py b/hoa/helpers/base.py
new file mode 100644
index 0000000..38e5c66
--- /dev/null
+++ b/hoa/helpers/base.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains helper functions."""
+
+import re
+
+
+def assert_(condition: bool, message: str = ""):
+    """User-defined assert."""
+    if not condition:
+        raise AssertionError(message)
+
+
+class RegexConstrainedString(str):
+    """
+    A string that is constrained by a regex.
+
+    The default behaviour is to match anything.
+    Subclass this class and change the 'REGEX' class
+    attribute to implement a different behaviour.
+    """
+
+    REGEX = re.compile(".*", flags=re.DOTALL)
+
+    def __new__(cls, value, *args, **kwargs):
+        """Instantiate a new object."""
+        if type(value) == cls:
+            return value
+        else:
+            inst = super(RegexConstrainedString, cls).__new__(cls, value)
+            return inst
+
+    def __init__(self, *_, **__):
+        """Initialize a regex constrained string."""
+        super().__init__()
+        if not self.REGEX.match(self):
+            self._handle_no_match()
+
+    def _handle_no_match(self):
+        raise ValueError(
+            "Value '{data}' does not match the regular expression {regex}".format(
+                data=self, regex=self.REGEX
+            )
+        )
+
+    def __instancecheck__(self, instance) -> bool:
+        """Check if a string satisfies the regex constraint."""
+        return isinstance(instance, str) and self.REGEX.match(instance) is not None
diff --git a/hoa/parsers.py b/hoa/parsers.py
new file mode 100644
index 0000000..c1e4dda
--- /dev/null
+++ b/hoa/parsers.py
@@ -0,0 +1,382 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the definition of the HOA parser."""
+
+import operator
+import os
+from collections import OrderedDict
+from enum import Enum
+from functools import reduce
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from lark import Lark, Transformer, Tree
+
+from hoa.ast.acceptance import (
+    AcceptanceAtom,
+    accepting_sets,
+    AtomType,
+    nb_accepting_sets,
+)
+from hoa.ast.boolean_expression import FalseFormula, TrueFormula
+from hoa.ast.label import LabelAlias, LabelAtom, LabelExpression
+from hoa.core import Acceptance, Edge, HOA, HOABody, HOAHeader, State
+from hoa.helpers.base import assert_
+from hoa.types import (
+    acceptance_parameter,
+    HEADER_VALUES,
+    hoa_header_value,
+    identifier,
+    string,
+)
+
+
+class HeaderItemType(Enum):
+    """Enumeration of header types."""
+
+    NUM_STATES = "num_states"
+    START_STATES = "start_state"
+    PROPOSITIONS = "propositions"
+    ALIAS = "alias"
+    ACCEPTANCE = "acceptance"
+    ACCEPTANCE_NAME = "acc_name"
+    TOOL = "tool"
+    NAME = "name"
+    PROPERTIES = "properties"
+    HEADERNAME = "headername"
+
+
+class HOATransformer(Transformer):
+    """The transformer of the AST of the tool to a more handy data structure."""
+
+    def __init__(self):
+        """Initialize the transformer."""
+        super().__init__(visit_tokens=True)
+
+        self._aliases: Dict[str, LabelExpression] = dict()
+
+    INT = int
+    STRING = string
+    IDENTIFIER = identifier
+    HEADERNAME = identifier
+
+    def BOOLEAN(self, args):
+        """Define BOOLEAN types."""
+        label = args[0]
+        if label == "t":
+            return True
+        if label == "f":
+            return False
+        raise ValueError("Should not be here.")
+
+    def start(self, args):
+        """Parse the 'start' node."""
+        return args[0]
+
+    def automaton(self, args):
+        """Parse the 'automaton' node."""
+        return HOA(args[0], args[1])
+
+    def header(self, args):
+        """Parse the 'header' node.
+
+        The header is a list of header-items (a HEADERNAME followed by some data).
+        Except for the "HOA:" item, which should always come first,
+        the items may occur in any order. Some HEADERNAMEs have predefined
+        semantics (and might be mandatory) as specified below. This format also
+        makes provision of additional (unspecified) header names to be used.
+
+        Any given HEADERNAME should occur at most once, except for Start:, Alias:, and properties:.
+        The case of the HEADERNAME's initial is used to specify whether tools may
+        safely ignore a header item they do not support: header items whose name
+        start with an upper-case letter are expected to influence the semantic of
+        the automaton: tools should at least warn about any such HEADERNAME they do not understand.
+        A HEADERNAME whose initial is lowercase may be safely ignored without affecting the semantics.
+
+        Headers items HOA:, and Acceptance: must always be present.
+        """
+        format_version = identifier(args[0])
+
+        headertype2value: Dict[HeaderItemType, Any] = {}
+        custom_headers: Optional[Dict[identifier, List[HEADER_VALUES]]] = dict()
+        for header_item_type, value in args[1:]:
+            if header_item_type in {
+                HeaderItemType.ALIAS,
+                HeaderItemType.PROPERTIES,
+            }:
+                headertype2value.setdefault(header_item_type, []).extend(
+                    value if isinstance(value, list) else [value]
+                )
+            elif header_item_type == HeaderItemType.START_STATES:
+                headertype2value.setdefault(header_item_type, set()).add(value)
+            elif header_item_type != HeaderItemType.HEADERNAME:
+                assert (
+                    header_item_type not in headertype2value
+                ), f"Header {header_item_type.value} occurred more than once."
+                headertype2value[header_item_type] = value
+            else:
+                key, arg_list = value
+                assert (
+                    key not in custom_headers
+                ), f"Custom header {key} occurred more than once."
+                custom_headers[key] = list(
+                    map(hoa_header_value.to_header_value, arg_list)
+                )
+
+        assert (
+            HeaderItemType.ACCEPTANCE in headertype2value
+        ), "'Acceptance:' must always be present."
+        acceptance_condition = headertype2value[HeaderItemType.ACCEPTANCE]
+        acceptance_args = headertype2value.get(HeaderItemType.ACCEPTANCE_NAME, None)
+        if acceptance_args is not None:
+            acceptance = Acceptance(
+                acceptance_condition,
+                acceptance_args[0],
+                tuple(
+                    map(acceptance_parameter.to_parameter_value, acceptance_args[1:])
+                ),
+            )
+        else:
+            acceptance = Acceptance(acceptance_condition)
+
+        return HOAHeader(
+            format_version,
+            acceptance=acceptance,
+            nb_states=headertype2value.get(HeaderItemType.NUM_STATES, None),
+            start_states=headertype2value.get(HeaderItemType.START_STATES, None),
+            aliases=headertype2value.get(HeaderItemType.ALIAS, None),
+            propositions=headertype2value.get(HeaderItemType.PROPOSITIONS, None),
+            tool=headertype2value.get(HeaderItemType.TOOL, None),
+            name=headertype2value.get(HeaderItemType.NAME, None),
+            properties=headertype2value.get(HeaderItemType.PROPERTIES, None),
+            headernames=custom_headers if custom_headers else None,
+        )
+
+    def format_version(self, args):
+        """Parse the 'format_version' node."""
+        return args[0]
+
+    def header_item(self, args):
+        """Parse the 'header_item' node."""
+        return args[0]
+
+    def num_states(self, args):
+        """Parse the 'num_states' node."""
+        return HeaderItemType.NUM_STATES, args[0]
+
+    def start_state(self, args):
+        """Parse the 'start_state' node."""
+        return HeaderItemType.START_STATES, frozenset(*args[::2])
+
+    def propositions(self, args):
+        """Parse the 'propositions' node."""
+        assert_(args[0] == len(args[1:]), "The number of propositions is not correct.")
+        assert_(
+            len(args[1:]) == len(set(args[1:])), "There are duplicate propositions."
+        )
+        return HeaderItemType.PROPOSITIONS, tuple(prop[1:-1] for prop in args[1:])
+
+    def alias(self, args):
+        """Parse the 'alias' node."""
+        alias_name = args[0]
+        label_expression = args[1]
+        label_alias = LabelAlias(alias_name, label_expression)
+        assert label_alias not in self._aliases, f"Alias {label_alias} defined twice!"
+        self._aliases[alias_name] = label_alias
+        return HeaderItemType.ALIAS, label_alias
+
+    def acceptance(self, args):
+        """Parse the 'acceptance' node."""
+        expected_nb_accepting_sets = args[0]
+        acceptance_condition = args[1]
+        actual_nb_accepting_sets = nb_accepting_sets(acceptance_condition)
+        accepting_sets_ = accepting_sets(acceptance_condition)
+        # this checks whether the number of acc. sets in the acceptance condition is correct.
+        assert_(
+            max(accepting_sets_)
+            == len(accepting_sets_) - 1
+            == expected_nb_accepting_sets - 1
+            == actual_nb_accepting_sets - 1
+        )
+        return HeaderItemType.ACCEPTANCE, acceptance_condition
+
+    def acc_name(self, args):
+        """Parse the 'acc_name' node."""
+        return HeaderItemType.ACCEPTANCE_NAME, args
+
+    def tool(self, args):
+        """Parse the 'tool' node."""
+        return HeaderItemType.TOOL, args[0]
+
+    def name(self, args):
+        """Parse the 'nome' node."""
+        return HeaderItemType.NAME, args[0].strip('"')
+
+    def properties(self, args):
+        """Parse the 'automaton' node."""
+        return HeaderItemType.PROPERTIES, args
+
+    def headername(self, args):
+        """Parse the 'automaton' node."""
+        key, values = args[0], args[1:]
+        return HeaderItemType.HEADERNAME, (key, values)
+
+    def body(self, args):
+        """Parse the 'body' node."""
+        state2edges: Dict[State, List[Edge]] = OrderedDict({})
+        current_state = None
+        for arg in args:
+            if isinstance(arg, State):
+                current_state = arg
+                state2edges[current_state] = []
+            elif isinstance(arg, Edge):
+                state2edges[current_state].append(arg)
+
+        return HOABody(state2edges)
+
+    def state_name(self, args):
+        """Parse the 'state_name' node."""
+        non_trees = [arg for arg in args if not isinstance(arg, Tree)]
+        kwargs = {arg.data: arg.children[0] for arg in args if isinstance(arg, Tree)}
+
+        if "acc_sig" in kwargs.keys():
+            kwargs["acc_sig"] = frozenset({kwargs["acc_sig"]})
+
+        if len(non_trees) == 1:
+            return State(index=non_trees[0], **kwargs)
+        elif len(non_trees) == 2:
+            return State(index=non_trees[0], name=non_trees[1].strip('"'), **kwargs)
+        else:
+            raise ValueError("Should not be here.")
+
+    def edge(self, args):
+        """Parse the 'edge' node."""
+        if len(args) == 1:
+            return Edge(args[0])
+        elif len(args) == 2:
+            # either 'label state_conj' or 'state_conj acc-sig'
+            first, second = args
+            if isinstance(first, Tree):
+                return Edge(second, label=first.children[0])
+            else:
+                return Edge(
+                    first, acc_sig=frozenset(second.children)
+                )  # acc_sig as frozenset()
+        elif len(args) == 3:
+            label, state_conj, acc_sig = (
+                args[0].children[0],
+                args[1],
+                frozenset(args[2].children),
+            )
+            return Edge(state_conj, label=label, acc_sig=acc_sig)
+        else:
+            raise ValueError("Should not be here.")
+
+    def state_conj(self, args):
+        """Parse the 'state_conj' node."""
+        # compute the flat list
+        return list(
+            reduce(
+                operator.add, map(lambda x: [x] if not isinstance(x, list) else x, args)
+            )
+        )
+
+    def label_expr(self, args):
+        """Parse the 'label_expr' node."""
+        return args[0]
+
+    def or_label_expr(self, args):
+        """Parse the 'or_label_expr' node."""
+        return reduce(operator.or_, args)
+
+    def and_label_expr(self, args):
+        """Parse the 'and_label_expr' node."""
+        return reduce(operator.and_, args)
+
+    def not_label_expr(self, args):
+        """Parse the 'not_label_expr' node."""
+        return ~args[0]
+
+    def alias_label_expr(self, args):
+        """Parse the 'alias_label_expr' node."""
+        if args[0] not in self._aliases:
+            raise ValueError(f"Alias {args[0]} not defined in the header.")
+        return self._aliases[args[0]]
+
+    def atom_label_expr(self, args):
+        """Parse the 'atom_label_expr' node."""
+        return LabelAtom(args[0])
+
+    def boolean_label_expr(self, args):
+        """Parse the 'boolean_label_expr' node."""
+        boolean = args[0]
+        if boolean is True:
+            return TrueFormula()
+        elif boolean is False:
+            return FalseFormula()
+        else:
+            raise ValueError("Should not be here.")
+
+    def acceptance_cond(self, args):
+        """Parse the 'acceptance_cond' node."""
+        return args[0]
+
+    def atom_acceptance_cond(self, args):
+        """Parse the 'atom_acceptance_cond' node."""
+        atom_type = AtomType(args[0])
+        accepting_set = args[1]
+        return AcceptanceAtom(atom_type, accepting_set, False)
+
+    def not_acceptance_cond(self, args):
+        """Parse the 'not_acceptance_cond' node."""
+        atom_type = AtomType(args[0])
+        accepting_set = args[1]
+        return AcceptanceAtom(accepting_set, atom_type, True)
+
+    def and_acceptance_cond(self, args):
+        """Parse the 'and_acceptance_cond' node."""
+        return reduce(operator.and_, args)
+
+    def or_acceptance_cond(self, args):
+        """Parse the 'or_acceptance_cond' node."""
+        return reduce(operator.or_, args)
+
+    def boolean_acceptance_cond(self, args):
+        """Parse the 'boolean_acceptance_cond' node."""
+        boolean = args[0]
+        assert_(type(boolean) == bool)
+        if boolean:
+            return TrueFormula()
+        else:
+            return FalseFormula()
+
+
+class HOAParser:
+    """The parser for the HOA format."""
+
+    def __init__(self):
+        """Initialize the HOA parser."""
+        directory = Path(os.path.dirname(os.path.realpath(__file__)))
+        self._transformer = HOATransformer()
+        self._parser = Lark(open(directory / "grammars" / "hoa.lark"))
+
+    def __call__(self, text: str):
+        """Try to parse a string."""
+        tree = self._parser.parse(text)
+        result = self._transformer.transform(tree)
+        return result
diff --git a/hoa/printers.py b/hoa/printers.py
new file mode 100644
index 0000000..fcab02e
--- /dev/null
+++ b/hoa/printers.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains functions to print acceptance conditions and label expressions."""
+
+from functools import singledispatch
+
+from hoa.ast.acceptance import AcceptanceAtom, AcceptanceCondition
+from hoa.ast.boolean_expression import BinaryOp, FalseFormula, TrueFormula, UnaryOp
+from hoa.ast.label import LabelAlias, LabelAtom, LabelExpression
+
+
+@singledispatch
+def acceptance_condition_to_string(_: AcceptanceCondition):
+    """Transform an acceptance condition into a string."""
+    return str(_)
+
+
+@acceptance_condition_to_string.register  # type: ignore
+def _(f: AcceptanceAtom):
+    """Transform an acceptance atom into a string."""
+    return f"{f.atom_type.value}({'!' if f.negated else ''}{f.acceptance_set})"
+
+
+@acceptance_condition_to_string.register  # type: ignore
+def _(f: BinaryOp):
+    """Transform a binary operation over acceptance formulas into a string."""
+    return (
+        "("
+        + f" {f.SYMBOL} ".join(map(acceptance_condition_to_string, f.operands))
+        + ")"
+    )
+
+
+@acceptance_condition_to_string.register  # type: ignore
+def _(_acceptance_condition: TrueFormula):
+    """Transform true into a string."""
+    return "t"
+
+
+@acceptance_condition_to_string.register  # type: ignore
+def _(_acceptance_condition: FalseFormula):
+    """Transform false into a string."""
+    return "f"
+
+
+@singledispatch
+def label_expression_to_string(_: LabelExpression):
+    """Transform a label expression into a string."""
+    return str(_)
+
+
+@label_expression_to_string.register  # type: ignore
+def _(f: LabelAtom):
+    """Transform a label atom into a string."""
+    return f"{f.proposition}"
+
+
+@label_expression_to_string.register  # type: ignore
+def _(f: LabelAlias):
+    """Transform a label alias into a string."""
+    return f"{f.alias}"
+
+
+@label_expression_to_string.register  # type: ignore
+def _(f: BinaryOp):
+    """Transform a binary operation over labels into a string."""
+    return "(" + f" {f.SYMBOL} ".join(map(label_expression_to_string, f.operands)) + ")"
+
+
+@label_expression_to_string.register  # type: ignore
+def _(f: UnaryOp):
+    """Transform a unary operation over labels into a string."""
+    return f"({f.SYMBOL}{label_expression_to_string(f.argument)})"
+
+
+@label_expression_to_string.register  # type: ignore
+def _(_f: TrueFormula):
+    """Transform a true formula into a string."""
+    return "t"
+
+
+@label_expression_to_string.register  # type: ignore
+def _(_f: FalseFormula):
+    """Transform a false formula into a string."""
+    return "f"
diff --git a/hoa/tools/__init__.py b/hoa/tools/__init__.py
new file mode 100644
index 0000000..eb31fa4
--- /dev/null
+++ b/hoa/tools/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This package contains the code for the CLI tools."""
diff --git a/hoa/tools/pyhoafparser.py b/hoa/tools/pyhoafparser.py
new file mode 100644
index 0000000..9432aee
--- /dev/null
+++ b/hoa/tools/pyhoafparser.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+"""This is the command line tool for translating HOA to DOT format."""
+from pathlib import Path
+
+import click
+
+from hoa.core import HOA
+from hoa.dumpers import dumps
+from hoa.parsers import HOAParser
+
+
+@click.command()
+@click.argument(
+    "input_file",
+    type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
+)
+@click.option(
+    "-o",
+    "--output",
+    type=click.Path(file_okay=True, dir_okay=False, writable=True),
+    help="Path to the output file.",
+)
+def main(input_file, output):
+    """Parse and validate a HOA file."""
+    input_string = Path(input_file).read_text()
+    file = Path(output) if output is not None else None
+    parser = HOAParser()
+    hoa_obj: HOA = parser(input_string)
+
+    if output is None:
+        print(dumps(hoa_obj))
+    else:
+        with file.open(mode="w") as fout:
+            print(dumps(hoa_obj), file=fout)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hoa/types.py b/hoa/types.py
new file mode 100644
index 0000000..765a914
--- /dev/null
+++ b/hoa/types.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+"""This module defines useful custom types."""
+import re
+from typing import Union
+
+from hoa.helpers.base import RegexConstrainedString
+
+
+class string(RegexConstrainedString):
+    r"""
+    This type represents a 'string' in a HOA file.
+
+    It must match the following regex: '(\\.|[^\\"])*'.
+    """
+
+    REGEX = re.compile('(\\.|[^\\"])*')
+
+
+class integer(RegexConstrainedString):
+    """
+    This type represents an 'integer' in a HOA file.
+
+    It must match the following regex: "0|[1-9][0-9]*".
+    """
+
+    REGEX = re.compile(r"0|[1-9][0-9]*")
+
+
+class boolean(RegexConstrainedString):
+    """
+    This type represents a 'boolean' in a HOA file.
+
+    It must match the following regex: "[tf]".
+    """
+
+    REGEX = re.compile("[tf]")
+
+
+class identifier(RegexConstrainedString):
+    """
+    This type represents an 'integer' in a HOA file.
+
+    It must match the following regex: "[a-zA-Z_][0-9a-zA-Z_-]*".
+    """
+
+    REGEX = re.compile("[a-zA-Z_][0-9a-zA-Z_-]*")
+
+
+class alias(RegexConstrainedString):
+    """
+    This type represents an 'alias' in a HOA file.
+
+    It must match the following regex: "@[0-9a-zA-Z_-]+".
+    """
+
+    REGEX = re.compile("@[0-9a-zA-Z_-]+")
+
+
+class headername(RegexConstrainedString):
+    """
+    This type represents a 'headername' in a HOA file.
+
+    It must match the following regex: "[a-zA-Z_][0-9a-zA-Z_-]*".
+    """
+
+    REGEX = re.compile("[a-zA-Z_][0-9a-zA-Z_-]*")
+
+
+class hoa_header_value(RegexConstrainedString):
+    """
+    This type represents a headername value in a HOA file.
+
+    It must match (BOOLEAN|INT|STRING|IDENTIFIER)
+    """
+
+    REGEX = re.compile(
+        f"({boolean.REGEX.pattern}|{integer.REGEX.pattern}|{string.REGEX.pattern}|{identifier.REGEX.pattern})"
+    )
+
+    @staticmethod
+    def to_header_value(value: "hoa_header_value") -> "HEADER_VALUES":
+        """
+        Convert a HOA header value string to a Python object.
+
+        :param s: the HOA header value string.
+        :return: the header value.
+        """
+        # from the stricter to the looser
+        s = str(value)
+        if re.match(boolean.REGEX, s):
+            return bool(s)
+        if re.match(integer.REGEX, s):
+            return int(s)
+        if re.match(identifier.REGEX, s):
+            return identifier(s)
+        if re.match(string.REGEX, s):
+            return string(s)
+        raise ValueError(f"Cannot parse headername value {s}")
+
+    @staticmethod
+    def to_hoa_header_value(v: "HEADER_VALUES") -> "hoa_header_value":
+        """
+        Convert a header value to its HOA header value string.
+
+        :param v: the header value.
+        :return: the HOA header value string.
+        """
+        if isinstance(v, bool):
+            return hoa_header_value("t" if v else "f")
+        if isinstance(v, int):
+            return hoa_header_value(str(v))
+        try:
+            return hoa_header_value(v)
+        except Exception as e:
+            raise ValueError(f"Cannot convert value {v}: {str(e)}")
+
+
+class proposition(RegexConstrainedString):
+    """
+    This type represents a 'proposition' in a HOA file.
+
+    It must match (BOOLEAN|INT|ANAME)
+    """
+
+    REGEX = re.compile(f"({boolean.REGEX}|{integer.REGEX}|{alias.REGEX})")
+
+
+class acceptance_parameter(RegexConstrainedString):
+    """
+    This type represents an 'acceptance parameter' in a HOA file.
+
+    It must match (IDENTIFIER | INT)
+    """
+
+    REGEX = re.compile(
+        f"({boolean.REGEX.pattern}|{identifier.REGEX.pattern}|{integer.REGEX.pattern})"
+    )
+
+    @staticmethod
+    def to_parameter_value(value: "hoa_header_value") -> "ACCEPTANCE_PARAMETER":
+        """
+        Convert a HOA acceptance parameter string to a Python object.
+
+        :param s: the HOA acceptance parameter string.
+        :return: the parameter value.
+        """
+        # from the stricter to the looser
+        s = str(value)
+        if re.match(boolean.REGEX, s):
+            return bool(s)
+        if re.match(integer.REGEX, s):
+            return int(s)
+        if re.match(identifier.REGEX, s):
+            return identifier(s)
+        raise ValueError(f"Cannot parse headername value {s}")
+
+    @staticmethod
+    def to_acceptance_parameter(v: "ACCEPTANCE_PARAMETER") -> "acceptance_parameter":
+        """
+        Convert an acceptance parameter to its HOA header value string.
+
+        :param v: the header value.
+        :return: the HOA header value string.
+        """
+        if isinstance(v, bool):
+            return acceptance_parameter("t" if v else "f")
+        if isinstance(v, int):
+            return acceptance_parameter(str(v))
+        try:
+            return acceptance_parameter(v)
+        except Exception as e:
+            raise ValueError(f"Cannot convert value {v}: {str(e)}")
+
+
+HEADER_VALUES = Union[bool, int, string, identifier]
+ACCEPTANCE_PARAMETER = Union[bool, int, identifier]
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..8ed1c36
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,14 @@
+site_name: HOA Utils
+repo_name: 'whitemech/hoa-utils'
+repo_url: https://github.com/whitemech/hoa-utils
+
+nav:
+- Home: index.md
+- Credits: authors.md
+- How to contribute: contributing.md
+
+theme: 
+  name: material
+markdown_extensions:
+- markdown_include.include:
+    base_path: docs
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..c435cc4
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1430 @@
+[[package]]
+category = "dev"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+name = "appdirs"
+optional = false
+python-versions = "*"
+version = "1.4.4"
+
+[[package]]
+category = "dev"
+description = "An abstract syntax tree for Python with inference support."
+name = "astroid"
+optional = false
+python-versions = ">=3.5"
+version = "2.4.2"
+
+[package.dependencies]
+lazy-object-proxy = ">=1.4.0,<1.5.0"
+six = ">=1.12,<2.0"
+wrapt = ">=1.11,<2.0"
+
+[package.dependencies.typed-ast]
+python = "<3.8"
+version = ">=1.4.0,<1.5"
+
+[[package]]
+category = "dev"
+description = "Atomic file writes."
+marker = "sys_platform == \"win32\""
+name = "atomicwrites"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.4.0"
+
+[[package]]
+category = "dev"
+description = "Classes Without Boilerplate"
+name = "attrs"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "20.1.0"
+
+[package.extras]
+dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
+
+[[package]]
+category = "dev"
+description = "Security oriented static analyser for python code."
+name = "bandit"
+optional = false
+python-versions = "*"
+version = "1.6.2"
+
+[package.dependencies]
+GitPython = ">=1.0.1"
+PyYAML = ">=3.13"
+colorama = ">=0.3.9"
+six = ">=1.10.0"
+stevedore = ">=1.20.0"
+
+[[package]]
+category = "dev"
+description = "The uncompromising code formatter."
+name = "black"
+optional = false
+python-versions = ">=3.6"
+version = "20.8b1"
+
+[package.dependencies]
+appdirs = "*"
+click = ">=7.1.2"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.6,<1"
+regex = ">=2020.1.8"
+toml = ">=0.10.1"
+typed-ast = ">=1.4.0"
+typing-extensions = ">=3.7.4"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
+
+[[package]]
+category = "dev"
+description = "Python package for providing Mozilla's CA Bundle."
+name = "certifi"
+optional = false
+python-versions = "*"
+version = "2020.6.20"
+
+[[package]]
+category = "dev"
+description = "Universal encoding detector for Python 2 and 3"
+name = "chardet"
+optional = false
+python-versions = "*"
+version = "3.0.4"
+
+[[package]]
+category = "main"
+description = "Composable command line interface toolkit"
+name = "click"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "7.1.2"
+
+[[package]]
+category = "dev"
+description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
+name = "codecov"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.1.9"
+
+[package.dependencies]
+coverage = "*"
+requests = ">=2.7.9"
+
+[[package]]
+category = "dev"
+description = "Cross-platform colored terminal text."
+marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
+name = "colorama"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "0.4.3"
+
+[[package]]
+category = "dev"
+description = "Code coverage measurement for Python"
+name = "coverage"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+version = "5.2.1"
+
+[package.extras]
+toml = ["toml"]
+
+[[package]]
+category = "dev"
+description = "Distribution utilities"
+name = "distlib"
+optional = false
+python-versions = "*"
+version = "0.3.1"
+
+[[package]]
+category = "dev"
+description = "A parser for Python dependency files"
+name = "dparse"
+optional = false
+python-versions = ">=3.5"
+version = "0.5.1"
+
+[package.dependencies]
+packaging = "*"
+pyyaml = "*"
+toml = "*"
+
+[package.extras]
+pipenv = ["pipenv"]
+
+[[package]]
+category = "dev"
+description = "Removes commented-out code."
+name = "eradicate"
+optional = false
+python-versions = "*"
+version = "1.0"
+
+[[package]]
+category = "dev"
+description = "A platform independent file lock."
+name = "filelock"
+optional = false
+python-versions = "*"
+version = "3.0.12"
+
+[[package]]
+category = "dev"
+description = "the modular source code checker: pep8 pyflakes and co"
+name = "flake8"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+version = "3.8.3"
+
+[package.dependencies]
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.6.0a1,<2.7.0"
+pyflakes = ">=2.2.0,<2.3.0"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = "*"
+
+[[package]]
+category = "dev"
+description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
+name = "flake8-bugbear"
+optional = false
+python-versions = ">=3.6"
+version = "20.1.4"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+flake8 = ">=3.0.0"
+
+[[package]]
+category = "dev"
+description = "Extension for flake8 which uses pydocstyle to check docstrings"
+name = "flake8-docstrings"
+optional = false
+python-versions = "*"
+version = "1.5.0"
+
+[package.dependencies]
+flake8 = ">=3"
+pydocstyle = ">=2.1"
+
+[[package]]
+category = "dev"
+description = "Flake8 plugin to find commented out code"
+name = "flake8-eradicate"
+optional = false
+python-versions = ">=3.5,<4.0"
+version = "0.4.0"
+
+[package.dependencies]
+attrs = "*"
+eradicate = ">=1.0,<2.0"
+flake8 = ">=3.5,<4.0"
+
+[[package]]
+category = "dev"
+description = "flake8 plugin that integrates isort ."
+name = "flake8-isort"
+optional = false
+python-versions = "*"
+version = "4.0.0"
+
+[package.dependencies]
+flake8 = ">=3.2.1,<4"
+isort = ">=4.3.5,<6"
+testfixtures = ">=6.8.0,<7"
+
+[package.extras]
+test = ["pytest (>=4.0.2,<6)", "toml"]
+
+[[package]]
+category = "dev"
+description = "Clean single-source support for Python 3 and 2"
+name = "future"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "0.18.2"
+
+[[package]]
+category = "dev"
+description = "Git Object Database"
+name = "gitdb"
+optional = false
+python-versions = ">=3.4"
+version = "4.0.5"
+
+[package.dependencies]
+smmap = ">=3.0.1,<4"
+
+[[package]]
+category = "dev"
+description = "Python Git Library"
+name = "gitpython"
+optional = false
+python-versions = ">=3.4"
+version = "3.1.7"
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+
+[[package]]
+category = "dev"
+description = "Internationalized Domain Names in Applications (IDNA)"
+name = "idna"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.10"
+
+[[package]]
+category = "dev"
+description = "Read metadata from Python packages"
+marker = "python_version < \"3.8\""
+name = "importlib-metadata"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+version = "1.7.0"
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "rst.linker"]
+testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
+
+[[package]]
+category = "dev"
+description = "iniconfig: brain-dead simple config-ini parsing"
+name = "iniconfig"
+optional = false
+python-versions = "*"
+version = "1.0.1"
+
+[[package]]
+category = "dev"
+description = "A Python utility / library to sort Python imports."
+name = "isort"
+optional = false
+python-versions = ">=3.6,<4.0"
+version = "5.4.2"
+
+[package.extras]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib", "tomlkit (>=0.5.3)"]
+requirements_deprecated_finder = ["pipreqs", "pip-api"]
+
+[[package]]
+category = "dev"
+description = "A very fast and expressive template engine."
+name = "jinja2"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.11.2"
+
+[package.dependencies]
+MarkupSafe = ">=0.23"
+
+[package.extras]
+i18n = ["Babel (>=0.8)"]
+
+[[package]]
+category = "dev"
+description = "Lightweight pipelining: using Python functions as pipeline jobs."
+marker = "python_version > \"2.7\""
+name = "joblib"
+optional = false
+python-versions = ">=3.6"
+version = "0.16.0"
+
+[[package]]
+category = "main"
+description = "a modern parsing library"
+name = "lark-parser"
+optional = false
+python-versions = "*"
+version = "0.9.0"
+
+[package.extras]
+regex = ["regex"]
+
+[[package]]
+category = "dev"
+description = "A fast and thorough lazy object proxy."
+name = "lazy-object-proxy"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.4.3"
+
+[[package]]
+category = "dev"
+description = "Python LiveReload is an awesome tool for web developers"
+name = "livereload"
+optional = false
+python-versions = "*"
+version = "2.6.3"
+
+[package.dependencies]
+six = "*"
+
+[package.dependencies.tornado]
+python = ">=2.8"
+version = "*"
+
+[[package]]
+category = "dev"
+description = "A Python implementation of Lunr.js"
+name = "lunr"
+optional = false
+python-versions = "*"
+version = "0.5.8"
+
+[package.dependencies]
+future = ">=0.16.0"
+six = ">=1.11.0"
+
+[package.dependencies.nltk]
+optional = true
+python = ">=2.8"
+version = ">=3.2.5"
+
+[package.extras]
+languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"]
+
+[[package]]
+category = "dev"
+description = "Python implementation of Markdown."
+name = "markdown"
+optional = false
+python-versions = ">=3.5"
+version = "3.2.2"
+
+[package.dependencies]
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = "*"
+
+[package.extras]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+category = "dev"
+description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator."
+name = "markdown-include"
+optional = false
+python-versions = "*"
+version = "0.6.0"
+
+[package.dependencies]
+markdown = "*"
+
+[[package]]
+category = "dev"
+description = "Safely add untrusted strings to HTML/XML markup."
+name = "markupsafe"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+version = "1.1.1"
+
+[[package]]
+category = "dev"
+description = "McCabe checker, plugin for flake8"
+name = "mccabe"
+optional = false
+python-versions = "*"
+version = "0.6.1"
+
+[[package]]
+category = "dev"
+description = "Project documentation with Markdown."
+name = "mkdocs"
+optional = false
+python-versions = ">=3.5"
+version = "1.1.2"
+
+[package.dependencies]
+Jinja2 = ">=2.10.1"
+Markdown = ">=3.2.1"
+PyYAML = ">=3.10"
+click = ">=3.3"
+livereload = ">=2.5.1"
+tornado = ">=5.0"
+
+[package.dependencies.lunr]
+extras = ["languages"]
+version = "0.5.8"
+
+[[package]]
+category = "dev"
+description = "A Material Design theme for MkDocs"
+name = "mkdocs-material"
+optional = false
+python-versions = "*"
+version = "5.5.11"
+
+[package.dependencies]
+Pygments = ">=2.4"
+markdown = ">=3.2"
+mkdocs = ">=1.1"
+mkdocs-material-extensions = ">=1.0"
+pymdown-extensions = ">=7.0"
+
+[[package]]
+category = "dev"
+description = "Extension pack for Python Markdown."
+name = "mkdocs-material-extensions"
+optional = false
+python-versions = ">=3.5"
+version = "1.0"
+
+[package.dependencies]
+mkdocs-material = ">=5.0.0"
+
+[[package]]
+category = "dev"
+description = "More routines for operating on iterables, beyond itertools"
+name = "more-itertools"
+optional = false
+python-versions = ">=3.5"
+version = "8.5.0"
+
+[[package]]
+category = "dev"
+description = "Optional static typing for Python"
+name = "mypy"
+optional = false
+python-versions = ">=3.5"
+version = "0.782"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3,<0.5.0"
+typed-ast = ">=1.4.0,<1.5.0"
+typing-extensions = ">=3.7.4"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+
+[[package]]
+category = "dev"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+name = "mypy-extensions"
+optional = false
+python-versions = "*"
+version = "0.4.3"
+
+[[package]]
+category = "dev"
+description = "Natural Language Toolkit"
+marker = "python_version > \"2.7\""
+name = "nltk"
+optional = false
+python-versions = "*"
+version = "3.5"
+
+[package.dependencies]
+click = "*"
+joblib = "*"
+regex = "*"
+tqdm = "*"
+
+[package.extras]
+all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"]
+corenlp = ["requests"]
+machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"]
+plot = ["matplotlib"]
+tgrep = ["pyparsing"]
+twitter = ["twython"]
+
+[[package]]
+category = "dev"
+description = "Core utilities for Python packages"
+name = "packaging"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "20.4"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+six = "*"
+
+[[package]]
+category = "dev"
+description = "Utility library for gitignore style pattern matching of file paths."
+name = "pathspec"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "0.8.0"
+
+[[package]]
+category = "dev"
+description = "Python Build Reasonableness"
+name = "pbr"
+optional = false
+python-versions = "*"
+version = "5.4.5"
+
+[[package]]
+category = "dev"
+description = "plugin and hook calling mechanisms for python"
+name = "pluggy"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "0.13.1"
+
+[package.dependencies]
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=0.12"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+category = "dev"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+name = "py"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.9.0"
+
+[[package]]
+category = "dev"
+description = "Python style guide checker"
+name = "pycodestyle"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.6.0"
+
+[[package]]
+category = "dev"
+description = "Python docstring style checker"
+name = "pydocstyle"
+optional = false
+python-versions = ">=3.5"
+version = "5.1.0"
+
+[package.dependencies]
+snowballstemmer = "*"
+
+[[package]]
+category = "dev"
+description = "passive checker of Python programs"
+name = "pyflakes"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.2.0"
+
+[[package]]
+category = "dev"
+description = "Pygments is a syntax highlighting package written in Python."
+name = "pygments"
+optional = false
+python-versions = ">=3.5"
+version = "2.6.1"
+
+[[package]]
+category = "dev"
+description = "python code static checker"
+name = "pylint"
+optional = false
+python-versions = ">=3.5.*"
+version = "2.6.0"
+
+[package.dependencies]
+astroid = ">=2.4.0,<=2.5"
+colorama = "*"
+isort = ">=4.2.5,<6"
+mccabe = ">=0.6,<0.7"
+toml = ">=0.7.1"
+
+[[package]]
+category = "dev"
+description = "Extension pack for Python Markdown."
+name = "pymdown-extensions"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+version = "8.0"
+
+[package.dependencies]
+Markdown = ">=3.2"
+
+[[package]]
+category = "dev"
+description = "Python parsing module"
+name = "pyparsing"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "2.4.7"
+
+[[package]]
+category = "dev"
+description = "pytest: simple powerful testing with Python"
+name = "pytest"
+optional = false
+python-versions = ">=3.5"
+version = "6.0.1"
+
+[package.dependencies]
+atomicwrites = ">=1.0"
+attrs = ">=17.4.0"
+colorama = "*"
+iniconfig = "*"
+more-itertools = ">=4.0.0"
+packaging = "*"
+pluggy = ">=0.12,<1.0"
+py = ">=1.8.2"
+toml = "*"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=0.12"
+
+[package.extras]
+checkqa_mypy = ["mypy (0.780)"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+category = "dev"
+description = "Pytest plugin for measuring coverage."
+name = "pytest-cov"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.10.1"
+
+[package.dependencies]
+coverage = ">=4.4"
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"]
+
+[[package]]
+category = "dev"
+description = "Pytest plugin to randomly order tests and control random.seed."
+name = "pytest-randomly"
+optional = false
+python-versions = ">=3.5"
+version = "3.4.1"
+
+[package.dependencies]
+pytest = "*"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = "*"
+
+[[package]]
+category = "dev"
+description = "YAML parser and emitter for Python"
+name = "pyyaml"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "5.3.1"
+
+[[package]]
+category = "dev"
+description = "Alternative regular expression module, to replace re."
+name = "regex"
+optional = false
+python-versions = "*"
+version = "2020.7.14"
+
+[[package]]
+category = "dev"
+description = "Python HTTP for Humans."
+name = "requests"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.24.0"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+chardet = ">=3.0.2,<4"
+idna = ">=2.5,<3"
+urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
+
+[package.extras]
+security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
+
+[[package]]
+category = "dev"
+description = "Checks installed dependencies for known vulnerabilities."
+name = "safety"
+optional = false
+python-versions = ">=3.5"
+version = "1.9.0"
+
+[package.dependencies]
+Click = ">=6.0"
+dparse = ">=0.5.1"
+packaging = "*"
+requests = "*"
+setuptools = "*"
+
+[[package]]
+category = "dev"
+description = "Python 2 and 3 compatibility utilities"
+name = "six"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "1.15.0"
+
+[[package]]
+category = "dev"
+description = "A pure Python implementation of a sliding window memory map manager"
+name = "smmap"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "3.0.4"
+
+[[package]]
+category = "dev"
+description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms."
+name = "snowballstemmer"
+optional = false
+python-versions = "*"
+version = "2.0.0"
+
+[[package]]
+category = "dev"
+description = "Manage dynamic plugins for Python applications"
+name = "stevedore"
+optional = false
+python-versions = ">=3.6"
+version = "3.2.0"
+
+[package.dependencies]
+pbr = ">=2.0.0,<2.1.0 || >2.1.0"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=1.7.0"
+
+[[package]]
+category = "dev"
+description = "A collection of helpers and mock objects for unit tests and doc tests."
+name = "testfixtures"
+optional = false
+python-versions = "*"
+version = "6.14.1"
+
+[package.extras]
+build = ["setuptools-git", "wheel", "twine"]
+docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"]
+test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"]
+
+[[package]]
+category = "dev"
+description = "Python Library for Tom's Obvious, Minimal Language"
+name = "toml"
+optional = false
+python-versions = "*"
+version = "0.10.1"
+
+[[package]]
+category = "dev"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+name = "tornado"
+optional = false
+python-versions = ">= 3.5"
+version = "6.0.4"
+
+[[package]]
+category = "dev"
+description = "tox is a generic virtualenv management and test command line tool"
+name = "tox"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+version = "3.19.0"
+
+[package.dependencies]
+colorama = ">=0.4.1"
+filelock = ">=3.0.0"
+packaging = ">=14"
+pluggy = ">=0.12.0"
+py = ">=1.4.17"
+six = ">=1.14.0"
+toml = ">=0.9.4"
+virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=0.12,<2"
+
+[package.extras]
+docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
+testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"]
+
+[[package]]
+category = "dev"
+description = "Fast, Extensible Progress Meter"
+marker = "python_version > \"2.7\""
+name = "tqdm"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*"
+version = "4.48.2"
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"]
+
+[[package]]
+category = "dev"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+name = "typed-ast"
+optional = false
+python-versions = "*"
+version = "1.4.1"
+
+[[package]]
+category = "dev"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+name = "typing-extensions"
+optional = false
+python-versions = "*"
+version = "3.7.4.3"
+
+[[package]]
+category = "dev"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+name = "urllib3"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+version = "1.25.10"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
+
+[[package]]
+category = "dev"
+description = "Virtual Python Environment builder"
+name = "virtualenv"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+version = "20.0.31"
+
+[package.dependencies]
+appdirs = ">=1.4.3,<2"
+distlib = ">=0.3.1,<1"
+filelock = ">=3.0.0,<4"
+six = ">=1.9.0,<2"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=0.12,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
+
+[[package]]
+category = "dev"
+description = "Find dead code"
+name = "vulture"
+optional = false
+python-versions = ">=3.6"
+version = "2.1"
+
+[package.dependencies]
+toml = "*"
+
+[[package]]
+category = "dev"
+description = "Module for decorators, wrappers and monkey patching."
+name = "wrapt"
+optional = false
+python-versions = "*"
+version = "1.12.1"
+
+[[package]]
+category = "dev"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+marker = "python_version < \"3.8\""
+name = "zipp"
+optional = false
+python-versions = ">=3.6"
+version = "3.1.0"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
+testing = ["jaraco.itertools", "func-timeout"]
+
+[metadata]
+content-hash = "9ed3d0b26a8084866b7689e20e903e9c03908b226e0b721c847b887de79d24df"
+python-versions = "^3.7"
+
+[metadata.files]
+appdirs = [
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+astroid = [
+    {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"},
+    {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"},
+]
+atomicwrites = [
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+    {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"},
+    {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"},
+]
+bandit = [
+    {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"},
+    {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"},
+]
+black = [
+    {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"},
+    {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
+]
+certifi = [
+    {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
+    {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
+]
+chardet = [
+    {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
+    {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
+]
+click = [
+    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
+    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
+]
+codecov = [
+    {file = "codecov-2.1.9-py2.py3-none-any.whl", hash = "sha256:24545847177a893716b3455ac5bfbafe0465f38d4eb86ea922c09adc7f327e65"},
+    {file = "codecov-2.1.9-py3.8.egg", hash = "sha256:7877f68effde3c2baadcff807a5d13f01019a337f9596eece0d64e57393adf3a"},
+    {file = "codecov-2.1.9.tar.gz", hash = "sha256:355fc7e0c0b8a133045f0d6089bde351c845e7b52b99fec5903b4ea3ab5f6aab"},
+]
+colorama = [
+    {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
+    {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
+]
+coverage = [
+    {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"},
+    {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"},
+    {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"},
+    {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"},
+    {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"},
+    {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"},
+    {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"},
+    {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"},
+    {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"},
+    {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"},
+    {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"},
+    {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"},
+    {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"},
+    {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"},
+    {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"},
+    {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"},
+    {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"},
+    {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"},
+    {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"},
+    {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"},
+    {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"},
+    {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"},
+    {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"},
+    {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"},
+    {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"},
+    {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"},
+    {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"},
+    {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"},
+    {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"},
+    {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"},
+    {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"},
+    {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"},
+    {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"},
+    {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"},
+]
+distlib = [
+    {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
+    {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
+]
+dparse = [
+    {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"},
+    {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"},
+]
+eradicate = [
+    {file = "eradicate-1.0.tar.gz", hash = "sha256:4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a"},
+]
+filelock = [
+    {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
+    {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
+]
+flake8 = [
+    {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"},
+    {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"},
+]
+flake8-bugbear = [
+    {file = "flake8-bugbear-20.1.4.tar.gz", hash = "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162"},
+    {file = "flake8_bugbear-20.1.4-py36.py37.py38-none-any.whl", hash = "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63"},
+]
+flake8-docstrings = [
+    {file = "flake8-docstrings-1.5.0.tar.gz", hash = "sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717"},
+    {file = "flake8_docstrings-1.5.0-py2.py3-none-any.whl", hash = "sha256:a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc"},
+]
+flake8-eradicate = [
+    {file = "flake8-eradicate-0.4.0.tar.gz", hash = "sha256:be5ea4521dfd4cb76837635f9ace57e12a7336c4b82054c99fd0394c00eef8ce"},
+    {file = "flake8_eradicate-0.4.0-py3-none-any.whl", hash = "sha256:804a39aef145c193e8d77770efc45df82e631b29c4456ecb5ff2e2e5996cf09a"},
+]
+flake8-isort = [
+    {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"},
+    {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"},
+]
+future = [
+    {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
+gitdb = [
+    {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
+    {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
+]
+gitpython = [
+    {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"},
+    {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"},
+]
+idna = [
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+]
+importlib-metadata = [
+    {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
+    {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
+]
+iniconfig = [
+    {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"},
+    {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"},
+]
+isort = [
+    {file = "isort-5.4.2-py3-none-any.whl", hash = "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750"},
+    {file = "isort-5.4.2.tar.gz", hash = "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8"},
+]
+jinja2 = [
+    {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
+    {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
+]
+joblib = [
+    {file = "joblib-0.16.0-py3-none-any.whl", hash = "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"},
+    {file = "joblib-0.16.0.tar.gz", hash = "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6"},
+]
+lark-parser = [
+    {file = "lark-parser-0.9.0.tar.gz", hash = "sha256:9e7589365d6b6de1cca40b0eaec31104a3fb96a37a11a9dfd5098e95b50aa6cd"},
+]
+lazy-object-proxy = [
+    {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
+    {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
+    {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"},
+    {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"},
+    {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"},
+    {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"},
+    {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"},
+    {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"},
+    {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"},
+    {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"},
+    {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"},
+    {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"},
+    {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"},
+    {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"},
+    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"},
+    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"},
+    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"},
+    {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"},
+    {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"},
+    {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
+    {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
+]
+livereload = [
+    {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
+]
+lunr = [
+    {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"},
+    {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
+]
+markdown = [
+    {file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"},
+    {file = "Markdown-3.2.2.tar.gz", hash = "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17"},
+]
+markdown-include = [
+    {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"},
+]
+markupsafe = [
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
+    {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
+]
+mccabe = [
+    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+mkdocs = [
+    {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"},
+    {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"},
+]
+mkdocs-material = [
+    {file = "mkdocs-material-5.5.11.tar.gz", hash = "sha256:dc87c10c67ec8b7158da9aa35d6fec8463c6fe5db0b674caba65c52d83a9c7fd"},
+    {file = "mkdocs_material-5.5.11-py2.py3-none-any.whl", hash = "sha256:e43346a5678280a7431673d36160dd6025f7f790d302be9be105fdb8b47b8c5d"},
+]
+mkdocs-material-extensions = [
+    {file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"},
+    {file = "mkdocs_material_extensions-1.0-py3-none-any.whl", hash = "sha256:09569c3694b5acc1e8334c9730e52b4bcde65fc9d613cc20e49af131ef1a9ca0"},
+]
+more-itertools = [
+    {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"},
+    {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"},
+]
+mypy = [
+    {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"},
+    {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"},
+    {file = "mypy-0.782-cp35-cp35m-win_amd64.whl", hash = "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d"},
+    {file = "mypy-0.782-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd"},
+    {file = "mypy-0.782-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"},
+    {file = "mypy-0.782-cp36-cp36m-win_amd64.whl", hash = "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406"},
+    {file = "mypy-0.782-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86"},
+    {file = "mypy-0.782-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707"},
+    {file = "mypy-0.782-cp37-cp37m-win_amd64.whl", hash = "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308"},
+    {file = "mypy-0.782-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc"},
+    {file = "mypy-0.782-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea"},
+    {file = "mypy-0.782-cp38-cp38-win_amd64.whl", hash = "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b"},
+    {file = "mypy-0.782-py3-none-any.whl", hash = "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d"},
+    {file = "mypy-0.782.tar.gz", hash = "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+nltk = [
+    {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"},
+]
+packaging = [
+    {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
+    {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
+]
+pathspec = [
+    {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
+    {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
+]
+pbr = [
+    {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"},
+    {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"},
+]
+pluggy = [
+    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+py = [
+    {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
+    {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
+]
+pycodestyle = [
+    {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
+    {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
+]
+pydocstyle = [
+    {file = "pydocstyle-5.1.0-py3-none-any.whl", hash = "sha256:08374b9d4d2b7164bae50b71bb24eb0d74a56b309029d5d502264092fa7db0c3"},
+    {file = "pydocstyle-5.1.0.tar.gz", hash = "sha256:4ca3c7736d36f92bb215dd74ef84ac3d6c146edd795c7afc5154c10f1eb1f65a"},
+]
+pyflakes = [
+    {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
+    {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
+]
+pygments = [
+    {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
+    {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
+]
+pylint = [
+    {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"},
+    {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"},
+]
+pymdown-extensions = [
+    {file = "pymdown-extensions-8.0.tar.gz", hash = "sha256:440b0db9823b89f9917482ce3ab3d32ac18e60f2e186770ac37836830d5e7256"},
+    {file = "pymdown_extensions-8.0-py2.py3-none-any.whl", hash = "sha256:c3b18b719adc3a441c4edbcf691157998dbd333384a8e2caf0690985a6e586f0"},
+]
+pyparsing = [
+    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+    {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"},
+    {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"},
+]
+pytest-cov = [
+    {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"},
+    {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"},
+]
+pytest-randomly = [
+    {file = "pytest-randomly-3.4.1.tar.gz", hash = "sha256:5fd0dbeeb218a7ce029690a8100453cd8d6a7972cf9d8e657690352692e92c69"},
+    {file = "pytest_randomly-3.4.1-py3-none-any.whl", hash = "sha256:2c4df1390db72a33a4f44fac0c780e7883cd5968238efa2a2bdbdd54e3fc6681"},
+]
+pyyaml = [
+    {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
+    {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"},
+    {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"},
+    {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"},
+    {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"},
+    {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"},
+    {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"},
+    {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"},
+    {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"},
+    {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
+    {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
+]
+regex = [
+    {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
+    {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
+    {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
+    {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
+    {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
+    {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
+    {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
+    {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
+    {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
+]
+requests = [
+    {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
+    {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
+]
+safety = [
+    {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"},
+    {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"},
+]
+six = [
+    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
+]
+smmap = [
+    {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"},
+    {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"},
+]
+snowballstemmer = [
+    {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
+    {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
+]
+stevedore = [
+    {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"},
+    {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"},
+]
+testfixtures = [
+    {file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"},
+    {file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"},
+]
+toml = [
+    {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
+    {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
+]
+tornado = [
+    {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"},
+    {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"},
+    {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"},
+    {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"},
+    {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"},
+    {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"},
+    {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"},
+    {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"},
+    {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"},
+]
+tox = [
+    {file = "tox-3.19.0-py2.py3-none-any.whl", hash = "sha256:3d94b6921a0b6dc90fd8128df83741f30bb41ccd6cd52d131a6a6944ca8f16e6"},
+    {file = "tox-3.19.0.tar.gz", hash = "sha256:17e61a93afe5c49281fb969ab71f7a3f22d7586d1c56f9a74219910f356fe7d3"},
+]
+tqdm = [
+    {file = "tqdm-4.48.2-py2.py3-none-any.whl", hash = "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf"},
+    {file = "tqdm-4.48.2.tar.gz", hash = "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21"},
+]
+typed-ast = [
+    {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
+    {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
+    {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
+    {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
+    {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
+    {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
+    {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
+    {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
+    {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
+    {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
+    {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
+    {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
+    {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
+    {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
+    {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
+    {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
+    {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
+    {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
+    {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
+    {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
+    {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
+]
+typing-extensions = [
+    {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
+    {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
+    {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
+]
+urllib3 = [
+    {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"},
+    {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"},
+]
+virtualenv = [
+    {file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"},
+    {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"},
+]
+vulture = [
+    {file = "vulture-2.1-py2.py3-none-any.whl", hash = "sha256:f4b3a422a6cb0285ec9df0ed46241a8577fa53ec3c43f8c70bd2a4d708c95377"},
+    {file = "vulture-2.1.tar.gz", hash = "sha256:933bf7f3848e9e39ecab6a12faa59d5185471c887534abac13baea6fe8138cc2"},
+]
+wrapt = [
+    {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
+]
+zipp = [
+    {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
+    {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1eb19b8
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,66 @@
+[tool.poetry]
+name = "hoa_utils"
+version = "0.1.0"
+description = "Utilities for the HOA format."
+authors = ["Marco Favorito <marco.favorito@gmail.com>", "Francesco Fuggitti <francesco.fuggitti@gmail.com>"]
+license = "LGPL-3.0-or-later"
+readme = "README.md"
+homepage = "https://whitemech.github.io/hoa-utils"
+repository = "https://github.com/whitemech/hoa-utils.git"
+documentation = "https://whitemech.github.io/hoa-utils"
+keywords = [
+    "HOA",
+    "automata",
+    "HOA-format",
+]
+classifiers = [
+    'Development Status :: 2 - Pre-Alpha',
+    'Intended Audience :: Developers',
+    'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)',
+    'Natural Language :: English',
+    'Programming Language :: Python :: 3',
+    'Programming Language :: Python :: 3.7',
+    'Programming Language :: Python :: 3.8',
+]
+packages = [
+    { include = "hoa" },
+]
+include = []
+
+[tool.poetry.scripts]
+pyhoafparser = 'hoa.tools.pyhoafparser:main'
+
+[tool.poetry.urls]
+"Bug Tracker" = "https://github.com/whitemech/hoa-utils/issues"
+"Pull Requests" = "https://github.com/whitemech/hoa-utils/pulls"
+
+[tool.poetry.dependencies]
+python = "^3.7"
+click = "^7.1.2"
+lark-parser = "^0.9.0"
+
+[tool.poetry.dev-dependencies]
+pytest = "^6.0.1"
+pytest-cov = "^2.10.1"
+pytest-randomly = "^3.4.1"
+tox = "^3.19.0"
+codecov = "^2.1.9"
+black = "==20.8b1"
+mypy = "^0.782"
+isort = "^5.4.2"
+flake8 = "^3.8.3"
+flake8-docstrings = "^1.5.0"
+flake8-bugbear = "^20.1.4"
+flake8-eradicate = "^0.4.0"
+flake8-isort = "^4.0.0"
+pylint = "^2.6.0"
+safety = "^1.9.0"
+vulture = "^2.1"
+bandit = "^1.6.2"
+mkdocs = "^1.1.2"
+markdown_include = "^0.6.0"
+mkdocs-material = "^5.5.11"
+
+[build-system]
+requires = ["poetry >= 0.12, <1"]
+build-backend = "poetry.masonry.api"
diff --git a/scripts/check_copyright.py b/scripts/check_copyright.py
new file mode 100644
index 0000000..f6d9de9
--- /dev/null
+++ b/scripts/check_copyright.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This script checks that all the Python files of the repository have the copyrigth notice.
+
+In particular
+- (optional) the Python shebang
+- the encoding header;
+- the copyright notice;
+
+It is assumed the script is run from the repository root.
+"""
+
+import itertools
+import re
+import sys
+from pathlib import Path
+
+HEADER_REGEX = r"""(#!/usr/bin/env python3
+)?# -\*- coding: utf-8 -\*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# \(at your option\) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#"""
+
+
+def check_copyright(file: Path) -> bool:
+    """
+    Given a file, check if the header stuff is in place.
+
+    Return True if the files has the encoding header and the copyright notice,
+    optionally prefixed by the shebang. Return False otherwise.
+
+    :param file: the file to check.
+    :return True if the file is compliant with the checks, False otherwise.
+    """
+    content = file.read_text()
+    header_regex = re.compile(HEADER_REGEX, re.MULTILINE)
+    return re.match(header_regex, content) is not None
+
+
+def parse_args():
+    """Parse arguments."""
+    import argparse  # pylint: disable=import-outside-toplevel
+
+    parser = argparse.ArgumentParser("check_copyright_notice")
+    parser.add_argument(
+        "--directory", type=str, default=".", help="The new_dir to the repository root."
+    )
+
+
+if __name__ == "__main__":
+    exclude_files = {Path("scripts", "whitelist.py")}
+    python_files = filter(
+        lambda x: x not in exclude_files,
+        itertools.chain(
+            Path("hoa").glob("**/*.py"),
+            Path("tests").glob("**/*.py"),
+            Path("scripts").glob("**/*.py"),
+        ),
+    )
+
+    bad_files = [filepath for filepath in python_files if not check_copyright(filepath)]
+
+    if len(bad_files) > 0:
+        print("The following files are not well formatted:")
+        print("\n".join(map(str, bad_files)))
+        sys.exit(1)
+    else:
+        print("OK")
+        sys.exit(0)
diff --git a/scripts/whitelist.py b/scripts/whitelist.py
new file mode 100644
index 0000000..d5c71eb
--- /dev/null
+++ b/scripts/whitelist.py
@@ -0,0 +1,31 @@
+# type: ignore
+NotFin  # unused function (hoa/ast/acceptance.py:77)
+NotInf  # unused function (hoa/ast/acceptance.py:94)
+_.__and__  # unused attribute (hoa/ast/boolean_expression.py:174)
+_.__or__  # unused attribute (hoa/ast/boolean_expression.py:176)
+_.__invert__  # unused attribute (hoa/ast/boolean_expression.py:178)
+dump  # unused function (hoa/dumpers.py:36)
+INT  # unused variable (hoa/parsers.py:75)
+STRING  # unused variable (hoa/parsers.py:76)
+IDENTIFIER  # unused variable (hoa/parsers.py:77)
+_.BOOLEAN  # unused method (hoa/parsers.py:80)
+_.start  # unused method (hoa/parsers.py:89)
+_.automaton  # unused method (hoa/parsers.py:93)
+_.header_item  # unused method (hoa/parsers.py:176)
+_.num_states  # unused method (hoa/parsers.py:180)
+_.start_state  # unused method (hoa/parsers.py:184)
+_.acc_name  # unused method (hoa/parsers.py:220)
+_.state_name  # unused method (hoa/parsers.py:254)
+_.label_expr  # unused method (hoa/parsers.py:301)
+_.or_label_expr  # unused method (hoa/parsers.py:305)
+_.and_label_expr  # unused method (hoa/parsers.py:309)
+_.not_label_expr  # unused method (hoa/parsers.py:313)
+_.alias_label_expr  # unused method (hoa/parsers.py:317)
+_.atom_label_expr  # unused method (hoa/parsers.py:323)
+_.boolean_label_expr  # unused method (hoa/parsers.py:327)
+_.acceptance_cond  # unused method (hoa/parsers.py:337)
+_.atom_acceptance_cond  # unused method (hoa/parsers.py:341)
+_.not_acceptance_cond  # unused method (hoa/parsers.py:347)
+_.and_acceptance_cond  # unused method (hoa/parsers.py:353)
+_.or_acceptance_cond  # unused method (hoa/parsers.py:357)
+_.boolean_acceptance_cond  # unused method (hoa/parsers.py:361)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..07df382
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,60 @@
+[bdist_wheel]
+universal = 1
+
+[aliases]
+test = pytest
+
+[metadata]
+license_file = LICENSE
+
+[flake8]
+ignore = W503
+exclude =
+    .tox,
+    .git,
+    __pycache__,
+    build,
+    dist,
+    tests/fixtures/*,
+    *.md,
+    *.pyc,
+    *.egg-info,
+    .cache,
+    .eggs,
+    hoa/__init__.py,
+    scripts/whitelist.py
+max-complexity = 10
+max-line-length = 120
+
+[isort]
+multi_line_output=3
+include_trailing_comma=True
+force_grid_wrap=0
+use_parentheses=True
+line_length=88
+order_by_type=False
+
+
+[black]
+exclude = "scripts/whitelist.py
+
+[mypy]
+python_version = 3.7
+strict_optional = True
+
+# Per-module options:
+
+
+[mypy-lark]
+ignore_missing_imports = True
+
+
+# Per-module options for tests dir:
+
+[mypy-pytest]
+ignore_missing_imports = True
+
+# Per-script options
+[mypy-scripts/whitelist]
+ignore_errors = True
+
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..488fb81
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,127 @@
+[tox]
+isolated_build = True
+envlist = bandit, safety, check-copyright, black-check, isort-check, vulture, flake8, mypy, py3.8, py3.7, docs
+; pylint TODO add
+
+[tox:.package]
+# note tox will use the same python version as under what tox is installed to package
+# so unless this is python 3 you can require a given python version for the packaging
+# environment via the basepython key
+basepython = python3
+
+[testenv]
+setenv =
+    PYTHONPATH = {toxinidir}
+deps =
+    pytest
+    pytest-cov
+    pytest-randomly
+    ; Main dependencies
+    click
+    lark-parser
+
+commands =
+    pytest --basetemp={envtmpdir} --doctest-modules \
+        hoa tests/ \
+        --cov=hoa \
+        --cov-report=xml \
+        --cov-report=html \
+        --cov-report=term
+
+[testenv:py3.6]
+basepython = python3.6
+
+[testenv:py3.7]
+basepython = python3.7
+
+[testenv:py3.8]
+basepython = python3.8
+
+[testenv:flake8]
+skip_install = True
+deps =
+    flake8
+    flake8-bugbear
+    flake8-docstrings
+    flake8-eradicate
+    flake8-isort
+    pydocstyle
+commands =
+    flake8 hoa tests scripts
+
+[testenv:mypy]
+skip_install = True
+deps =
+    mypy
+commands =
+    mypy hoa tests scripts
+
+[testenv:pylint]
+skipdist = True
+deps = pylint
+commands = pylint hoa tests scripts
+
+[testenv:black]
+skip_install = True
+deps = black==20.8b1
+commands = black hoa tests scripts
+
+[testenv:black-check]
+skip_install = True
+deps = black==20.8b1
+commands = black hoa tests scripts --check --verbose
+
+[testenv:isort]
+skip_install = True
+deps = isort
+commands = isort hoa tests scripts
+
+[testenv:isort-check]
+skip_install = True
+deps = isort
+commands = isort --check-only hoa tests scripts
+
+[testenv:bandit]
+skipsdist = True
+skip_install = True
+deps = bandit
+commands = bandit hoa tests scripts
+
+[testenv:safety]
+skipsdist = True
+skip_install = True
+deps = safety
+commands = safety
+
+[testenv:docs]
+skip_install = True
+deps =
+    mkdocs
+    mkdocs-material
+    markdown-include
+    pymdown-extensions
+commands =
+    mkdocs build --clean
+
+[testenv:docs-serve]
+skip_install = True
+deps =
+    mkdocs
+    mkdocs-material
+    markdown-include
+    pymdown-extensions
+commands =
+    mkdocs build --clean
+    python -c 'print("###### Starting local server. Press Control+C to stop server ######")'
+    mkdocs serve
+
+[testenv:check-copyright]
+skip_install = True
+deps =
+commands = python3 scripts/check_copyright.py
+
+[testenv:vulture]
+skipsdist = True
+skip_install = True
+deps = vulture
+commands = vulture hoa scripts/whitelist.py


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..df34471
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,38 @@
+name: test
+
+on:
+  push:
+    branches:
+      - develop
+      - master
+  pull_request:
+
+jobs:
+  run:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: [3.7, 3.8]
+
+    timeout-minutes: 30
+
+    steps:
+    - uses: actions/checkout@master
+    - uses: actions/setup-python@master
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run:  pip install tox poetry
+    - name: Unit tests and coverage
+      run: |
+        tox -e py${{ matrix.python-version }}
+    - name: Upload coverage to Codecov
+      uses: codecov/codecov-action@v1
+      with:
+        token: ${{ secrets.CODECOV_TOKEN }}
+        file: ./coverage.xml
+        flags: unittests
+        name: codecov-umbrella
+        fail_ci_if_error: true
\ No newline at end of file
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..b014ae3
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+log_cli = 1
+log_cli_level = DEBUG
+log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
+log_cli_date_format=%Y-%m-%d %H:%M:%S
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..26e3325
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the tests for the hoa tool."""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..1c5e85b
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the configurations for the tests."""
+import inspect
+import os
+from pathlib import Path
+
+TEST_ROOT_DIR = os.path.dirname(inspect.getfile(inspect.currentframe()))  # type: ignore
+ROOT_DIR = str(Path(TEST_ROOT_DIR, "..").resolve())  # type: ignore
+
+HOA_FILES = [
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut1.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut2.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut3.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut3.2.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut4.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut5.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut6.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut7.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut8.hoa")),
+    Path(os.path.join(TEST_ROOT_DIR, "examples", "aut11.hoa")),
+]
diff --git a/tests/examples/aut1.hoa b/tests/examples/aut1.hoa
new file mode 100644
index 0000000..d95cf33
--- /dev/null
+++ b/tests/examples/aut1.hoa
@@ -0,0 +1,15 @@
+HOA: v1
+States: 2
+Start: 0
+acc-name: Rabin 1
+Acceptance: 2 (Fin(0) & Inf(1) & t)
+AP: 2 "a" "b"
+Alias: @a 0
+Alias: @b 1
+--BODY--
+State: 0 "a U b"   /* An example of named state */
+  [0 & !1] 0 {0}
+  [1] 1 {0}
+State: 1
+  [t] 1 {1}
+--END--
diff --git a/tests/examples/aut11.hoa b/tests/examples/aut11.hoa
new file mode 100644
index 0000000..e039a42
--- /dev/null
+++ b/tests/examples/aut11.hoa
@@ -0,0 +1,19 @@
+HOA: v1
+name: "(Fa & G(b&Xc)) | c"
+States: 4
+Start: 0&2
+Start: 3
+acc-name: co-Buchi
+Acceptance: 1 Fin(0)
+AP: 3 "a" "b" "c"
+--BODY--
+State: 0 "Fa"
+[t] 0 {0}
+[0] 1
+State: 1 "true"
+[t] 1
+State: 2 "G(b&Xc)"
+[1] 2&3
+State: 3 "c"
+[2] 1
+--END--
diff --git a/tests/examples/aut2.hoa b/tests/examples/aut2.hoa
new file mode 100644
index 0000000..023a31f
--- /dev/null
+++ b/tests/examples/aut2.hoa
@@ -0,0 +1,17 @@
+HOA: v1
+States: 3
+Start: 0
+acc-name: Rabin 1
+Acceptance: 2 (Fin(0) & Inf(1))
+AP: 2 "a" "b"
+--BODY--
+State: 0 "a U b" { 0 }
+  2  /* !a  & !b */
+  0  /*  a  & !b */
+  1  /* !a  &  b */
+  1  /*  a  &  b */
+State: 1 { 1 }
+  1 1 1 1       /* four transitions on one line */
+State: 2 "sink state" { 0 }
+  2 2 2 2
+--END--
diff --git a/tests/examples/aut3.2.hoa b/tests/examples/aut3.2.hoa
new file mode 100644
index 0000000..7cc8c8d
--- /dev/null
+++ b/tests/examples/aut3.2.hoa
@@ -0,0 +1,14 @@
+HOA: v1
+name: "GFa & GFb"
+States: 1
+Start: 0
+acc-name: generalized-Buchi 2
+Acceptance: 2 (Inf(0) & Inf(1))
+AP: 2 "a" "b"
+--BODY--
+State: 0
+[!0 & !1] 0
+[0 & !1]  0 {0}
+[!0 & 1]  0 {1}
+[0 & 1]   0 {0 1}
+--END--
diff --git a/tests/examples/aut3.hoa b/tests/examples/aut3.hoa
new file mode 100644
index 0000000..3397551
--- /dev/null
+++ b/tests/examples/aut3.hoa
@@ -0,0 +1,14 @@
+HOA: v1
+name: "GFa & GFb"
+States: 1
+Start: 0
+acc-name: generalized-Buchi 2
+Acceptance: 2 (Inf(0) & Inf(1))
+AP: 2 "a" "b"
+--BODY--
+State: 0
+  0       /* !a  & !b */
+  0 {0}   /*  a  & !b */
+  0 {1}   /* !a  &  b */
+  0 {0 1} /*  a  &  b */
+--END--
diff --git a/tests/examples/aut4.hoa b/tests/examples/aut4.hoa
new file mode 100644
index 0000000..ca1f864
--- /dev/null
+++ b/tests/examples/aut4.hoa
@@ -0,0 +1,16 @@
+HOA: v1
+name: "GFa & GF(b & c)"
+States: 1
+Start: 0
+acc-name: generalized-Buchi 2
+Acceptance: 2 (Inf(0) & Inf(1))
+AP: 3 "a" "b" "c"
+Alias: @a 0
+Alias: @bc 1 & 2
+--BODY--
+State: 0
+[!@a & !@bc] 0
+[@a & !@bc]  0 {0}
+[!@a & @bc]  0 {1}
+[@a & @bc]   0 {0 1}
+--END--
diff --git a/tests/examples/aut5.hoa b/tests/examples/aut5.hoa
new file mode 100644
index 0000000..70eb980
--- /dev/null
+++ b/tests/examples/aut5.hoa
@@ -0,0 +1,14 @@
+HOA: v1
+name: "GFa"
+States: 2
+Start: 0
+Start: 1
+acc-name: Buchi
+Acceptance: 1 Inf(0)
+AP: 1 "a"
+--BODY--
+State: [0] 0 {0}
+  0 1
+State: [!0] 1
+  0 1
+--END--
diff --git a/tests/examples/aut6.hoa b/tests/examples/aut6.hoa
new file mode 100644
index 0000000..5b3ca05
--- /dev/null
+++ b/tests/examples/aut6.hoa
@@ -0,0 +1,17 @@
+HOA: v1
+States: 3
+Start: 0
+acc-name: Buchi
+Acceptance: 1 Inf(0)
+AP: 1 "a"
+--BODY--
+State: 0
+ [0] 1
+ [!0]  2
+State: 1  /* former state 0 */
+ [0] 1 {0}
+ [!0] 2 {0}
+State: 2  /* former state 1 */
+ [0] 1
+ [!0] 2
+--END--
diff --git a/tests/examples/aut7.hoa b/tests/examples/aut7.hoa
new file mode 100644
index 0000000..58c0711
--- /dev/null
+++ b/tests/examples/aut7.hoa
@@ -0,0 +1,22 @@
+HOA: v1
+name: "GFa | G(b <-> Xa)"
+Start: 0
+acc-name: Buchi
+Acceptance: 1 Inf(0)
+AP: 2 "a" "b"
+properties: explicit-labels trans-labels
+--BODY--
+State: 0
+ [t] 1
+ [1] 2
+ [!1] 3
+State: 1 "GFa"
+ [0] 1 {0}
+ [!0] 1
+State: 2 "a & G(b <-> Xa)" {0}
+ [0&1] 2
+ [0&!1] 3
+State: 3 "!a & G(b <-> Xa)" {0}
+ [!0&1] 2
+ [!0&!1] 3
+--END--
diff --git a/tests/examples/aut8.hoa b/tests/examples/aut8.hoa
new file mode 100644
index 0000000..dd150b8
--- /dev/null
+++ b/tests/examples/aut8.hoa
@@ -0,0 +1,22 @@
+HOA: v1
+name: "GFa | G(b <-> Xa)"
+Start: 0
+acc-name: Buchi
+Acceptance: 1 Inf(0)
+AP: 2 "a" "b"
+properties: explicit-labels trans-labels trans-acc
+--BODY--
+State: 0
+ [t] 1
+ [1] 2
+ [!1] 3
+State: 1 "GFa"
+ [0] 1 {0}
+ [!0] 1
+State: 2 "a & G(b <-> Xa)"
+ [0&1] 2 {0}
+ [0&!1] 3 {0}
+State: 3 "!a & G(b <-> Xa)"
+ [!0&1] 2 {0}
+ [!0&!1] 3 {0}
+--END--
diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py
new file mode 100644
index 0000000..6f3a07c
--- /dev/null
+++ b/tests/test_acceptance.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the test for the 'hoa.ast.acceptance' module."""
+
+from hoa.ast.acceptance import Acceptance, accepting_sets, Fin, Inf, NotFin, NotInf
+from hoa.ast.boolean_expression import PositiveAnd, PositiveOr
+
+
+def test_accepting_sets():
+    """Test the accepting sets."""
+    fin0 = Fin(0)
+    inf1 = Inf(1)
+    not_fin0 = NotFin(0)
+    not_inf1 = NotInf(1)
+
+    and_ = fin0 & ~inf1
+    or_ = not_fin0 | not_inf1 | and_
+
+    assert isinstance(and_, PositiveAnd)
+    assert isinstance(or_, PositiveOr)
+
+    assert accepting_sets(or_) == {0, 1}
+
+
+def test_acceptance():
+    """Test Acceptance instantiation."""
+    fin0 = Fin(0)
+    Acceptance(fin0, name="fin 0")
diff --git a/tests/test_label.py b/tests/test_label.py
new file mode 100644
index 0000000..1556d09
--- /dev/null
+++ b/tests/test_label.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains the test for the 'hoa.ast.label' module."""
+
+from hoa.ast.boolean_expression import And, Not, Or
+from hoa.ast.label import LabelAlias, LabelAtom, propositions
+from hoa.types import alias
+
+
+def test_propositions():
+    """Test the accepting sets."""
+    a = LabelAtom(0)
+    b = LabelAtom(1)
+    a_and_b = a & b
+    c = LabelAtom(2)
+    d = LabelAlias(alias("@d"), a & b)
+
+    or_ = c | d
+    not_ = ~or_
+
+    assert isinstance(a_and_b, And)
+    assert isinstance(or_, Or)
+    assert isinstance(not_, Not)
+
+    assert propositions(not_) == {0, 1, 2}
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
new file mode 100644
index 0000000..f91cbef
--- /dev/null
+++ b/tests/test_parsing.py
@@ -0,0 +1,586 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""Test the parsing module."""
+from collections import OrderedDict
+from io import StringIO
+from pathlib import Path
+
+import pytest
+
+from hoa.ast.acceptance import Fin, Inf, nb_accepting_sets
+from hoa.ast.boolean_expression import TRUE
+from hoa.ast.label import LabelAlias, LabelAtom
+from hoa.core import Acceptance, Edge, HOA, HOABody, HOAHeader, State
+from hoa.dumpers import dump
+from hoa.parsers import HOAParser
+from hoa.types import alias, identifier, string
+
+from .conftest import HOA_FILES, TEST_ROOT_DIR
+
+
+@pytest.mark.parametrize(
+    "filepath",
+    HOA_FILES,
+)
+def test_parsing_is_deterministic(filepath):
+    """Test that parsing is deterministic."""
+    parser = HOAParser()
+    hoa_obj_1 = parser(open(filepath).read())  # type: HOA
+    temp = StringIO()
+    dump(hoa_obj_1, temp)
+    temp.seek(0)
+    hoa_obj_2 = parser(temp.read())
+    assert hoa_obj_1 == hoa_obj_2
+
+
+class TestParsingAut1:
+    """Test parsing for tests/examples/aut1."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj: HOA = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut1.hoa").read_text()
+        )
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_format_version(self):
+        """Test that the format version is correct."""
+        assert self.hoa_header.format_version == "v1"
+
+    def test_nb_states(self):
+        """Test that the number of states is correct."""
+        assert self.hoa_header.nb_states == 2
+
+    def test_start_states(self):
+        """Test that the initial states are correct."""
+        assert self.hoa_header.start_states == {frozenset([0])}
+
+    def test_acc_name(self):
+        """Test that the acc-name is correct."""
+        assert self.hoa_header.acceptance.name == "Rabin"
+
+    def test_acceptance(self):
+        """Test that the acceptance condition is correct."""
+        assert nb_accepting_sets(self.hoa_header.acceptance.condition) == 2
+        assert self.hoa_header.acceptance == Acceptance(
+            Fin(0) & Inf(1) & TRUE, identifier("Rabin"), (1,)
+        )
+
+    def test_AP(self):
+        """Test that AP is correct."""
+        assert self.hoa_header.propositions == ("a", "b")
+        assert len(self.hoa_header.propositions) == 2
+
+    def test_aliases(self):
+        """Test that Alias is correct."""
+        assert self.hoa_header.aliases[0] == LabelAlias(alias("@a"), LabelAtom(0))
+        assert self.hoa_header.aliases[1] == LabelAlias(alias("@b"), LabelAtom(1))
+
+    def test_body(self):
+        """Test that the HOA body is correct."""
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0, name=string("a U b"))] = [
+            Edge(
+                [0],
+                LabelAtom(0) & ~LabelAtom(1),
+                {0},
+            ),
+            Edge([1], LabelAtom(1), {0}),
+        ]
+        state_edges_dict[State(1)] = [Edge([1], TRUE, {1})]
+        assert self.hoa_body.state2edges == state_edges_dict
+
+
+class TestParsingAut2:
+    """Test parsing for tests/examples/aut2."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj: HOA = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut2.hoa").read_text()
+        )
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_format_version(self):
+        """Test that the format version is correct."""
+        assert self.hoa_header.format_version == "v1"
+
+    def test_nb_states(self):
+        """Test that the number of states is correct."""
+        assert self.hoa_header.nb_states == 3
+
+    def test_start_states(self):
+        """Test that the initial states are correct."""
+        assert self.hoa_header.start_states == {frozenset({0})}
+
+    def test_acc_name(self):
+        """Test that the acc-name is correct."""
+        assert self.hoa_header.acceptance.name == "Rabin"
+
+    def test_acceptance(self):
+        """Test that the acceptance condition is correct."""
+        assert nb_accepting_sets(self.hoa_header.acceptance.condition) == 2
+        assert self.hoa_header.acceptance == Acceptance(
+            Fin(0) & Inf(1), identifier("Rabin"), (1,)
+        )
+
+    def test_AP(self):
+        """Test that AP is correct."""
+        assert self.hoa_header.propositions == ("a", "b")
+
+    def test_body(self):
+        """Test that the HOA body is correct."""
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0, name=string("a U b"), acc_sig=frozenset({0}))] = [
+            Edge([2]),
+            Edge([0]),
+            Edge([1]),
+            Edge([1]),
+        ]
+
+        state_edges_dict[State(1, acc_sig=frozenset({1}))] = [
+            Edge([1]),
+            Edge([1]),
+            Edge([1]),
+            Edge([1]),
+        ]
+        state_edges_dict[
+            State(2, name=string("sink state"), acc_sig=frozenset({0}))
+        ] = [
+            Edge([2]),
+            Edge([2]),
+            Edge([2]),
+            Edge([2]),
+        ]
+
+        assert self.hoa_body.state2edges == state_edges_dict
+
+
+class TestParsingAut3:
+    """Test parsing for tests/examples/aut3."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj: HOA = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut3.hoa").read_text()
+        )
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa_header(self):
+        """Test that the HOA header is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            acceptance=Acceptance(
+                Inf(0) & Inf(1), identifier("generalized-Buchi"), (2,)
+            ),
+            nb_states=1,
+            start_states={frozenset([0])},
+            propositions=(string("a"), string("b")),
+            name=string("GFa & GFb"),
+        )
+        assert self.hoa_header == hoa_header
+
+    def test_hoa_body(self):
+        """Test that the HOA body is correct."""
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0)] = [
+            Edge([0]),
+            Edge([0], acc_sig=frozenset({0})),
+            Edge([0], acc_sig=frozenset({1})),
+            Edge([0], acc_sig=frozenset({0, 1})),
+        ]
+        hoa_body = HOABody(state_edges_dict)
+        assert self.hoa_body == hoa_body
+
+
+class TestParsingAut3_2:
+    """Test parsing for tests/examples/aut3.2."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj: HOA = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut3.2.hoa").read_text()
+        )
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa_header(self):
+        """Test that the HOA header is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Inf(0) & Inf(1), identifier("generalized-Buchi"), (2,)),
+            nb_states=1,
+            start_states={frozenset([0])},
+            propositions=(string("a"), string("b")),
+            name=string("GFa & GFb"),
+        )
+        assert self.hoa_header == hoa_header
+
+    def test_hoa_body(self):
+        """Test that the HOA body is correct."""
+        a0, a1 = LabelAtom(0), LabelAtom(1)
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0)] = [
+            Edge(
+                [0],
+                label=~a0 & ~a1,
+            ),
+            Edge(
+                [0],
+                label=a0 & ~a1,
+                acc_sig={0},
+            ),
+            Edge(
+                [0],
+                label=~a0 & a1,
+                acc_sig={1},
+            ),
+            Edge(
+                [0],
+                label=a0 & a1,
+                acc_sig={0, 1},
+            ),
+        ]
+        hoa_body = HOABody(state_edges_dict)
+        assert self.hoa_body == hoa_body
+
+
+class TestParsingAut4:
+    """Test parsing for tests/examples/aut4."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut4.hoa").read_text()
+        )  # type: HOA
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+        cls.alias_a = LabelAlias(alias("@a"), LabelAtom(0))
+        cls.alias_b = LabelAlias(alias("@bc"), LabelAtom(1) & LabelAtom(2))
+
+    def test_hoa_header(self):
+        """Test that the HOA header is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Inf(0) & Inf(1), identifier("generalized-Buchi"), (2,)),
+            nb_states=1,
+            start_states={frozenset([0])},
+            propositions=(string("a"), string("b"), string("c")),
+            name=string("GFa & GF(b & c)"),
+            aliases=[
+                self.alias_a,
+                self.alias_b,
+            ],
+        )
+        assert self.hoa_header == hoa_header
+
+    def test_hoa_body(self):
+        """Test that the HOA body is correct."""
+        state_edges_dict = OrderedDict({})
+        hoa_body = HOABody(state_edges_dict)
+        state_edges_dict[State(0)] = [
+            Edge(
+                [0],
+                label=~self.alias_a & ~self.alias_b,
+            ),
+            Edge(
+                [0],
+                label=self.alias_a & ~self.alias_b,
+                acc_sig={0},
+            ),
+            Edge(
+                [0],
+                label=~self.alias_a & self.alias_b,
+                acc_sig={1},
+            ),
+            Edge(
+                [0],
+                label=self.alias_a & self.alias_b,
+                acc_sig={0, 1},
+            ),
+        ]
+        assert self.hoa_body == hoa_body
+
+
+class TestParsingAut5:
+    """Test parsing for tests/examples/aut5."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut5.hoa").read_text()
+        )  # type: HOA
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa_header(self):
+        """Test that the HOA header is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Inf(0), identifier("Buchi")),
+            nb_states=2,
+            start_states={frozenset([0]), frozenset([1])},
+            propositions=(string("a"),),
+            name=string("GFa"),
+        )
+        assert self.hoa_header == hoa_header
+
+    def test_hoa_body(self):
+        """Test that the HOA body is correct."""
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0, label=LabelAtom(0), acc_sig=frozenset({0}))] = [
+            Edge([0]),
+            Edge([1]),
+        ]
+        state_edges_dict[State(1, label=~LabelAtom(0))] = [
+            Edge([0]),
+            Edge([1]),
+        ]
+        hoa_body = HOABody(state_edges_dict)
+        assert self.hoa_body == hoa_body
+
+
+class TestParsingAut6:
+    """Test parsing for tests/examples/aut6."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut6.hoa").read_text()
+        )  # type: HOA
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa(self):
+        """Test that the HOA automaton is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Inf(0), identifier("Buchi")),
+            nb_states=3,
+            start_states={frozenset([0])},
+            propositions=(string("a"),),
+        )
+
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0)] = [
+            Edge([1], label=LabelAtom(0)),
+            Edge([2], label=~(LabelAtom(0))),
+        ]
+        state_edges_dict[State(1)] = [
+            Edge([1], label=LabelAtom(0), acc_sig=frozenset({0})),
+            Edge(
+                [2],
+                label=~(LabelAtom(0)),
+                acc_sig=frozenset({0}),
+            ),
+        ]
+        state_edges_dict[State(2)] = [
+            Edge([1], label=LabelAtom(0)),
+            Edge([2], label=~LabelAtom(0)),
+        ]
+        hoa_body = HOABody(state_edges_dict)
+
+        hoa_obj = HOA(hoa_header, hoa_body)
+        assert self.hoa_obj == hoa_obj
+
+
+class TestParsingAut7:
+    """Test parsing for tests/examples/aut7."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut7.hoa").read_text()
+        )  # type: HOA
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa(self):
+        """Test that the HOA automaton is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Inf(0), identifier("Buchi")),
+            start_states={frozenset([0])},
+            propositions=(string("a"), string("b")),
+            name=string("GFa | G(b <-> Xa)"),
+            properties=[identifier("explicit-labels"), identifier("trans-labels")],
+        )
+
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0)] = [
+            Edge([1], label=TRUE),
+            Edge([2], label=LabelAtom(1)),
+            Edge([3], label=~LabelAtom(1)),
+        ]
+        state_edges_dict[State(1, name=string("GFa"))] = [
+            Edge([1], label=LabelAtom(0), acc_sig=frozenset({0})),
+            Edge([1], label=~LabelAtom(0)),
+        ]
+        state_edges_dict[
+            State(2, name=string("a & G(b <-> Xa)"), acc_sig=frozenset({0}))
+        ] = [
+            Edge(
+                [2],
+                label=LabelAtom(0) & LabelAtom(1),
+            ),
+            Edge([3], label=LabelAtom(0) & ~(LabelAtom(1))),
+        ]
+        state_edges_dict[
+            State(3, name=string("!a & G(b <-> Xa)"), acc_sig=frozenset({0}))
+        ] = [
+            Edge(
+                [2],
+                label=~(LabelAtom(0)) & LabelAtom(1),
+            ),
+            Edge(
+                [3],
+                label=~(LabelAtom(0)) & ~LabelAtom(1),
+            ),
+        ]
+        hoa_body = HOABody(state_edges_dict)
+
+        hoa_obj = HOA(hoa_header, hoa_body)
+        assert self.hoa_obj == hoa_obj
+
+
+class TestParsingAut8:
+    """Test parsing for tests/examples/aut8."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut8.hoa").read_text()
+        )  # type: HOA
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa(self):
+        """Test that the HOA automaton is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Inf(0), identifier("Buchi")),
+            start_states={frozenset([0])},
+            propositions=(string("a"), string("b")),
+            name=string("GFa | G(b <-> Xa)"),
+            properties=[
+                identifier("explicit-labels"),
+                identifier("trans-labels"),
+                identifier("trans-acc"),
+            ],
+        )
+
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0)] = [
+            Edge([1], label=TRUE),
+            Edge([2], label=LabelAtom(1)),
+            Edge([3], label=~(LabelAtom(1))),
+        ]
+        state_edges_dict[State(1, name=string("GFa"))] = [
+            Edge([1], label=LabelAtom(0), acc_sig=frozenset({0})),
+            Edge([1], label=~(LabelAtom(0))),
+        ]
+        state_edges_dict[State(2, name=string("a & G(b <-> Xa)"))] = [
+            Edge(
+                [2],
+                label=LabelAtom(0) & LabelAtom(1),
+                acc_sig=frozenset({0}),
+            ),
+            Edge(
+                [3],
+                label=LabelAtom(0) & ~LabelAtom(1),
+                acc_sig=frozenset({0}),
+            ),
+        ]
+        state_edges_dict[State(3, name=string("!a & G(b <-> Xa)"))] = [
+            Edge(
+                [2],
+                label=~LabelAtom(0) & LabelAtom(1),
+                acc_sig=frozenset({0}),
+            ),
+            Edge(
+                [3],
+                label=~LabelAtom(0) & ~LabelAtom(1),
+                acc_sig=frozenset({0}),
+            ),
+        ]
+        hoa_body = HOABody(state_edges_dict)
+
+        hoa_obj = HOA(hoa_header, hoa_body)
+        assert self.hoa_obj == hoa_obj
+
+
+class TestParsingAut11:
+    """Test parsing for tests/examples/aut11."""
+
+    @classmethod
+    def setup_class(cls):
+        """Set the test up."""
+        parser = HOAParser()
+        cls.hoa_obj: HOA = parser(
+            Path(TEST_ROOT_DIR, "examples", "aut11.hoa").read_text()
+        )
+        cls.hoa_header = cls.hoa_obj.header
+        cls.hoa_body = cls.hoa_obj.body
+
+    def test_hoa(self):
+        """Test that the HOA automaton is correct."""
+        hoa_header = HOAHeader(
+            identifier("v1"),
+            Acceptance(Fin(0), identifier("co-Buchi")),
+            nb_states=4,
+            start_states={frozenset([0, 2]), frozenset([3])},
+            propositions=(string("a"), string("b"), string("c")),
+            name=string("(Fa & G(b&Xc)) | c"),
+        )
+
+        state_edges_dict = OrderedDict({})
+        state_edges_dict[State(0, name=string("Fa"))] = [
+            Edge([0], label=TRUE, acc_sig=frozenset({0})),
+            Edge([1], label=LabelAtom(0)),
+        ]
+        state_edges_dict[State(1, name=string("true"))] = [Edge([1], label=TRUE)]
+        state_edges_dict[State(2, name=string("G(b&Xc)"))] = [
+            Edge([2, 3], label=LabelAtom(1))
+        ]
+        state_edges_dict[State(3, name=string("c"))] = [Edge([1], label=LabelAtom(2))]
+        hoa_body = HOABody(state_edges_dict)
+
+        hoa_obj = HOA(hoa_header, hoa_body)
+        assert self.hoa_obj == hoa_obj
diff --git a/tests/test_printer.py b/tests/test_printer.py
new file mode 100644
index 0000000..37f59bd
--- /dev/null
+++ b/tests/test_printer.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""Test the printer module."""
+from hoa.ast.acceptance import Fin, NotInf
+from hoa.ast.label import LabelAlias, LabelAtom
+from hoa.printers import acceptance_condition_to_string, label_expression_to_string
+from hoa.types import alias
+
+
+def test_printer_acceptance():
+    """Test the printer for acceptance conditions."""
+    fin0 = Fin(0)
+    notinf0 = NotInf(1)
+    and_ = fin0 & notinf0
+    or_ = fin0 | and_
+
+    expected = "(Fin(0) | (Fin(0) & Inf(!1)))"
+    actual = acceptance_condition_to_string(or_)
+    assert expected == actual
+
+
+def test_printer_label():
+    """Test the printer for label expressions."""
+    a = LabelAtom(0)
+    b = LabelAtom(1)
+    c = LabelAtom(2)
+    d = LabelAlias(alias("@d"), a & b)
+
+    or_ = c | d
+    not_ = ~or_
+
+    expected = "(!(2 | @d))"
+    actual = label_expression_to_string(not_)
+    assert actual == expected
diff --git a/tests/test_pyhoafparser.py b/tests/test_pyhoafparser.py
new file mode 100644
index 0000000..a980423
--- /dev/null
+++ b/tests/test_pyhoafparser.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""Test the pyhoafparser tool."""
+import shutil
+import tempfile
+from io import StringIO
+from pathlib import Path
+from unittest import mock
+from unittest.mock import MagicMock
+
+import pytest
+from click.testing import CliRunner
+
+from hoa.dumpers import dump
+from hoa.tools.pyhoafparser import main
+from tests.conftest import HOA_FILES
+from tests.test_utils import cd
+
+
+@mock.patch("hoa.dumpers.dumps", return_value="example")
+def test_dump(*_mocks):
+    """Test the dump method."""
+    hoa_object = MagicMock()
+    fp = StringIO()
+    dump(hoa_object, fp)
+    fp.seek(0)
+    assert fp.read() == "example"
+
+
+def test_which_pyhoafparser():
+    """Test 'pyhoafparser' is in the PATH."""
+    assert shutil.which("pyhoafparser") is not None
+
+
+@pytest.mark.parametrize(
+    "filepath",
+    HOA_FILES,
+)
+def test_pyhoafparser_positive(filepath):
+    """Test pyhoafparser, positive case."""
+    runner = CliRunner()
+    result = runner.invoke(main, [str(filepath.absolute())])
+    assert result.exit_code == 0, result.exc_info
+
+
+@pytest.mark.parametrize(
+    "filepath",
+    HOA_FILES,
+)
+def test_pyhoafparser_positive_with_file(filepath):
+    """Test pyhoafparser, positive case, with output file."""
+    runner = CliRunner()
+    with tempfile.TemporaryDirectory() as temp_dir:
+        output_file = "output"
+        with cd(temp_dir):
+            result = runner.invoke(
+                main, [str(filepath.absolute()), "--output", output_file]
+            )
+        assert result.exit_code == 0, result.exc_info
+        assert Path(temp_dir, output_file).exists()
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..04c886d
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# This file is part of hoa-utils.
+#
+# hoa-utils is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# hoa-utils is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with hoa-utils.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+"""This module contains test utils."""
+import os
+from contextlib import contextmanager
+from pathlib import Path
+
+
+@contextmanager
+def cd(new_dir: Path):
+    """
+    Change directory with a context manager.
+
+    :param new_dir: the new directory where to go.
+    :return: None
+    """
+    old_dir = os.getcwd()
+    try:
+        os.chdir(str(new_dir))
+        yield
+    finally:
+        os.chdir(old_dir)