From 9c9b915a27512d1d4afbbac3130aa6eac496d4dc Mon Sep 17 00:00:00 2001 From: Nathan Roach Date: Tue, 28 Dec 2021 12:52:01 -0500 Subject: [PATCH] Initial commit --- .github/workflows/pythonpackage.yml | 82 ++ .gitignore | 132 +++ .readthedocs.yml | 12 + LICENSE | 24 + README.md | 60 ++ ci/check.sh | 57 ++ ci/flake8.cfg | 7 + ci/mypy.ini | 6 + docs/Makefile | 88 ++ docs/api.rst | 9 + docs/conf.py | 220 +++++ docs/index.rst | 17 + docs/installation.rst | 27 + poetry.lock | 1010 ++++++++++++++++++++++ pybedlite/__init__.py | 93 ++ pybedlite/bed_record.py | 190 ++++ pybedlite/bed_source.py | 165 ++++ pybedlite/bed_writer.py | 144 +++ pybedlite/overlap_detector.py | 224 +++++ pybedlite/tests/__init__.py | 1 + pybedlite/tests/test_overlap_detector.py | 136 +++ pybedlite/tests/test_pybedlite.py | 256 ++++++ pyproject.toml | 47 + 23 files changed, 3007 insertions(+) create mode 100644 .github/workflows/pythonpackage.yml create mode 100644 .gitignore create mode 100644 .readthedocs.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100755 ci/check.sh create mode 100644 ci/flake8.cfg create mode 100644 ci/mypy.ini create mode 100644 docs/Makefile create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 poetry.lock create mode 100644 pybedlite/__init__.py create mode 100644 pybedlite/bed_record.py create mode 100644 pybedlite/bed_source.py create mode 100644 pybedlite/bed_writer.py create mode 100644 pybedlite/overlap_detector.py create mode 100644 pybedlite/tests/__init__.py create mode 100644 pybedlite/tests/test_overlap_detector.py create mode 100644 pybedlite/tests/test_pybedlite.py create mode 100644 pyproject.toml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000..64f74f7 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,82 @@ +name: Python package + +on: [push] +env: + POETRY_VERSION: 1.1 + +jobs: + testing: + runs-on: ubuntu-latest + strategy: + matrix: + PYTHON_VERSION: ["3.6", "3.7"] + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{matrix.PYTHON_VERSION}} + uses: actions/setup-python@v1 + with: + python-version: ${{matrix.PYTHON_VERSION}} + + - name: Get full Python version + id: full-python-version + shell: bash + run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") + + - name: Install poetry + shell: bash + run: | + python -m pip install --upgrade pip + pip install poetry==${{env.POETRY_VERSION}} + + - name: Configure poetry + shell: bash + run: poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@v2 + id: cache + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + shell: bash + run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv + + - name: Install deps + shell: bash + run: | + poetry install --extras docs + + - name: Run pytest + shell: bash + run: | + poetry run python -m pytest --cov=pybedlite --cov-report=xml --cov-branch + + - name: Style checking + shell: bash + run: | + poetry run black --line-length 99 --check pybedlite + + - name: Run lint + shell: bash + run: | + poetry run flake8 --config=ci/flake8.cfg pybedlite + + - name: Run mypy + shell: bash + run: | + poetry run mypy -p pybedlite --config=ci/mypy.ini + + - name: Run docs + shell: bash + run: | + set -euo pipefail + pushd docs + poetry run make html + popd + + - name: Upload code coverage + uses: codecov/codecov-action@v2.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e54d62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# VSCode configurations +.vscode/ \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..55b93b5 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +build: + image: latest +version: 2 +sphinx: + configuration: docs/conf.py +python: + version: 3.6 + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0b1e7a --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2021 Fulcrum Genomics LLC, with the exception of + - pybedlite/overlap_detector.py + - pybedlite/tests/test_overlap_detector.py + which are Copyright 2020 Myriad Genetics Inc. and are copied from https://github.com/myriad-opensource/samwell under MIT license. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f16efa --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ + +[![Language][language-badge]][language-link] +[![Code Style][code-style-badge]][code-style-link] +[![Type Checked][type-checking-badge]][type-checking-link] +[![PEP8][pep-8-badge]][pep-8-link] +[![Code Coverage][code-coverage-badge]][code-coverage-link] +[![License][license-badge]][license-link] + +--- + +[![Python package][python-package-badge]][python-package-link] +[![PyPI version][pypi-badge]][pypi-link] +[![PyPI download total][pypi-downloads-badge]][pypi-downloads-link] + +--- + +[language-badge]: http://img.shields.io/badge/language-python-brightgreen.svg +[language-link]: http://www.python.org/ +[code-style-badge]: https://img.shields.io/badge/code%20style-black-000000.svg +[code-style-link]: https://black.readthedocs.io/en/stable/ +[type-checking-badge]: http://www.mypy-lang.org/static/mypy_badge.svg +[type-checking-link]: http://mypy-lang.org/ +[pep-8-badge]: https://img.shields.io/badge/code%20style-pep8-brightgreen.svg +[pep-8-link]: https://www.python.org/dev/peps/pep-0008/ +[code-coverage-badge]: https://codecov.io/gh/fulcrumgenomics/pybedlite/branch/master/graph/badge.svg +[code-coverage-link]: https://codecov.io/gh/fulcrumgenomics/pybedlite +[license-badge]: http://img.shields.io/badge/license-MIT-blue.svg +[license-link]: https://github.com/fulcrumgenomics/pybedlite/blob/master/LICENSE +[python-package-badge]: https://github.com/fulcrumgenomics/pybedlite/workflows/Python%20package/badge.svg +[python-package-link]: https://github.com/fulcrumgenomics/pybedlite/actions?query=workflow%3A%22Python+package%22 +[pypi-badge]: https://badge.fury.io/py/pybedlite.svg +[pypi-link]: https://pypi.python.org/pypi/pybedlite +[pypi-downloads-badge]: https://img.shields.io/pypi/dm/pybedlite +[pypi-downloads-link]: https://pypi.python.org/pypi/pybedlite + +# pybedlite + +`pip install pybedlite` + +**Requires python 3.6+** + +# Getting Setup + +[Poetry][poetry-link] is used to manage the python development environment. + +A simple way to create an environment with the desired version of python and poetry is to use [conda][conda-link]. E.g.: + +```bash +conda create -n pybedlite python=3.6 poetry +conda activate pybedlite +poetry install +``` + +If, during `poetry install` on Mac OS X errors are encountered running gcc/clang to build `pybedtools` or other packages with native code, try setting the following and re-running `poetry install`: +```bash +export CFLAGS="-stdlib=libc++" +``` + +[poetry-link]: https://github.com/python-poetry/poetry +[conda-link]: https://docs.conda.io/en/latest/miniconda.html \ No newline at end of file diff --git a/ci/check.sh b/ci/check.sh new file mode 100755 index 0000000..9da7a8b --- /dev/null +++ b/ci/check.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +function banner() { + echo + echo "================================================================================" + echo $* + echo "================================================================================" + echo +} + +##################################################################### +# Takes two parameters, a "name" and a "command". +# Runs the command and prints out whether it succeeded or failed, and +# also tracks a list of failed steps in $failures. +##################################################################### +function run() { + local name=$1 + local cmd=$2 + + banner "Running $name [$cmd]" + set +e + $cmd + exit_code=$? + set -e + + if [[ $exit_code == 0 ]]; then + echo Passed $name: "[$cmd]" + else + echo Failed $name: "[$cmd]" + if [ -z "$failures" ]; then + failures="$failures $name" + else + failures="$failures, $name" + fi + fi +} + +parent=$(cd $(dirname $0) && pwd -P) + +# If the script is invoked with --check only have black check, otherwise have it fix! +black_extra_args="" +if [[ "$1" == "--check" ]]; then + black_extra_args="--check" +fi + +banner "Executing in conda environment ${CONDA_DEFAULT_ENV} in directory ${root}" +run "Unit Tests" "python -m pytest -vv -r sx pybedlite" +run "Style Checking" "black --line-length 99 $black_extra_args pybedlite" +run "Linting" "flake8 --config=$parent/flake8.cfg pybedlite" +run "Type Checking" "mypy -p pybedlite --config $parent/mypy.ini" + +if [ -z "$failures" ]; then + banner "Checks Passed" +else + banner "Checks Failed with failures in: $failures" + exit 1 +fi diff --git a/ci/flake8.cfg b/ci/flake8.cfg new file mode 100644 index 0000000..5bebb97 --- /dev/null +++ b/ci/flake8.cfg @@ -0,0 +1,7 @@ +# flake8 config file + +[flake8] +max_line_length = 100 +show-source = true +ignore = E701 W504 W503 +extend-ignore = E203 diff --git a/ci/mypy.ini b/ci/mypy.ini new file mode 100644 index 0000000..ed67815 --- /dev/null +++ b/ci/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +strict_optional = False +ignore_missing_imports = True +disallow_untyped_decorators = False +follow_imports = silent +disallow_untyped_defs = True diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..7b70c27 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,88 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf _build/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html + @echo + @echo "Build finished. The HTML pages are in _build/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml + @echo + @echo "Build finished. The HTML pages are in _build/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in _build/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in _build/qthelp, like this:" + @echo "# qcollectiongenerator _build/qthelp/samtools.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile _build/qthelp/samtools.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + @echo + @echo "Build finished; the LaTeX files are in _build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes + @echo + @echo "The overview file is in _build/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in _build/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in _build/doctest/output.txt." diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..ea764a8 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,9 @@ +=== +API +=== + +.. automodule:: pybedlite + :members: + +.. automodule:: pybedlite.overlap_detector + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d4310aa --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# +# pybedlite documentation build configuration file +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os, glob + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#_libdir = "../build/lib.%s-%s-%s.%s" % (os.uname()[0].lower(), os.uname()[4], +# sys.version_info[0], sys.version_info[1]) +_libdir = "../build/lib" +if os.path.exists(_libdir): + sys.path.insert(0, os.path.abspath(_libdir)) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.todo', + 'sphinx.ext.ifconfig', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon'] + +intersphinx_mapping = {'python': ('http://docs.python.org/3.6', None)} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pybedlite' +copyright = u'2021, Fulcrum Genomics' + +# Included at the end of each rst file +rst_epilog = ''' +.. _pybedlite: https://github.com/fulcrumgenomics/pybedlite +.. _python: http://python.org/ +.. _conda: https://conda.io/docs/ +''' + +autosummary_generate = True + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +from pathlib import Path +import os +toml_path = Path(os.path.realpath(__file__)).parent.parent / 'pyproject.toml' +with toml_path.open("r") as reader: + for line in reader: + if line.startswith("version"): + version = line.rstrip("\r\n").split(" = ")[1] + version = version[1:-1] + break + +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +# unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_use_modindex = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pybedlitedoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +# latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +# latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pybedlite.tex', u'pybedlite documentation', u'Fulcrum Genomics', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +# latex_preamble = '' + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_use_modindex = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d74f6fb --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +========= +pybedlite +========= + + +:Date: |today| +:Version: |version| + +Documentation Contents +====================== + +.. toctree:: + :maxdepth: 2 + + api.rst + installation.rst + diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..5f72930 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,27 @@ +============ +Installation +============ + +**Requires python 3.6+** + +Install with:: + + pip install pybedlite` + + +Getting Setup +============= + +`Poetry `_ is used to manage the python development environment. + +A simple way to create an environment with the desired version of python and poetry is to use `conda `_. +E.g.:: + + conda create -n pybedlite python=3.6 poetry + conda activate pybedlite + poetry install + +If, during `poetry install` on Mac OS X errors are encountered running gcc/clang to build `pybedtools` or other packages with native code, try setting the following and re-running `poetry install`:: + + export CFLAGS="-stdlib=libc++" + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..9eb60b2 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1010 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.3.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "babel" +version = "2.9.1" +description = "Internationalization utilities" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +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]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = true +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "dev" +optional = false +python-versions = ">=3.6, <3.7" + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = true +python-versions = ">=3.5" + +[[package]] +name = "imagesize" +version = "1.3.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "intervaltree" +version = "3.1.0" +description = "Editable interval tree data structure for Python 2 and 3" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +sortedcontainers = ">=2.0,<3.0" + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A very fast and expressive template engine." +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = true +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.930" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = ">=1.1.0" +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = true +python-versions = ">=3.5" + +[[package]] +name = "pyparsing" +version = "3.0.6" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytz" +version = "2021.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "regex" +version = "2021.11.10" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "sphinx" +version = "4.3.1" +description = "Python documentation generator" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.18" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "1.2.3" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typed-ast" +version = "1.5.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-inspect" +version = "0.7.1" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "1.22" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = true +python-versions = "*" + +[package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[extras] +docs = ["sphinx"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.6.0" +content-hash = "5fa32fb98242b4de0ea971859363e0c07000a9440c244bb8909451e26819a597" + +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +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-21.3.0-py2.py3-none-any.whl", hash = "sha256:8f7335278dedd26b58c38e006338242cc0977f06d51579b2b8b87b9b33bff66c"}, + {file = "attrs-21.3.0.tar.gz", hash = "sha256:50f3c9b216dc9021042f71b392859a773b904ce1a029077f58f6598272432045"}, +] +babel = [ + {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, + {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, +] +click = [ + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, +] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +imagesize = [ + {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, + {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +intervaltree = [ + {file = "intervaltree-3.1.0.tar.gz", hash = "sha256:902b1b88936918f9b2a19e0e5eb7ccb430ae45cde4f39ea4b36932920d33952d"}, +] +jinja2 = [ + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy = [ + {file = "mypy-0.930-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221cc94dc6a801ccc2be7c0c9fd791c5e08d1fa2c5e1c12dec4eab15b2469871"}, + {file = "mypy-0.930-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db3a87376a1380f396d465bed462e76ea89f838f4c5e967d68ff6ee34b785c31"}, + {file = "mypy-0.930-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d2296f35aae9802eeb1327058b550371ee382d71374b3e7d2804035ef0b830b"}, + {file = "mypy-0.930-cp310-cp310-win_amd64.whl", hash = "sha256:959319b9a3cafc33a8185f440a433ba520239c72e733bf91f9efd67b0a8e9b30"}, + {file = "mypy-0.930-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:45a4dc21c789cfd09b8ccafe114d6de66f0b341ad761338de717192f19397a8c"}, + {file = "mypy-0.930-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1e689e92cdebd87607a041585f1dc7339aa2e8a9f9bad9ba7e6ece619431b20c"}, + {file = "mypy-0.930-cp36-cp36m-win_amd64.whl", hash = "sha256:ed4e0ea066bb12f56b2812a15ff223c57c0a44eca817ceb96b214bb055c7051f"}, + {file = "mypy-0.930-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a9d8dffefba634b27d650e0de2564379a1a367e2e08d6617d8f89261a3bf63b2"}, + {file = "mypy-0.930-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b419e9721260161e70d054a15abbd50603c16f159860cfd0daeab647d828fc29"}, + {file = "mypy-0.930-cp37-cp37m-win_amd64.whl", hash = "sha256:601f46593f627f8a9b944f74fd387c9b5f4266b39abad77471947069c2fc7651"}, + {file = "mypy-0.930-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ea7199780c1d7940b82dbc0a4e37722b4e3851264dbba81e01abecc9052d8a7"}, + {file = "mypy-0.930-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70b197dd8c78fc5d2daf84bd093e8466a2b2e007eedaa85e792e513a820adbf7"}, + {file = "mypy-0.930-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5feb56f8bb280468fe5fc8e6f56f48f99aa0df9eed3c507a11505ee4657b5380"}, + {file = "mypy-0.930-cp38-cp38-win_amd64.whl", hash = "sha256:2e9c5409e9cb81049bb03fa1009b573dea87976713e3898561567a86c4eaee01"}, + {file = "mypy-0.930-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:554873e45c1ca20f31ddf873deb67fa5d2e87b76b97db50669f0468ccded8fae"}, + {file = "mypy-0.930-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0feb82e9fa849affca7edd24713dbe809dce780ced9f3feca5ed3d80e40b777f"}, + {file = "mypy-0.930-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc1a0607ea03c30225347334af66b0af12eefba018a89a88c209e02b7065ea95"}, + {file = "mypy-0.930-cp39-cp39-win_amd64.whl", hash = "sha256:f9f665d69034b1fcfdbcd4197480d26298bbfb5d2dfe206245b6498addb34999"}, + {file = "mypy-0.930-py3-none-any.whl", hash = "sha256:bf4a44e03040206f7c058d1f5ba02ef2d1820720c88bc4285c7d9a4269f54173"}, + {file = "mypy-0.930.tar.gz", hash = "sha256:51426262ae4714cc7dd5439814676e0992b55bcc0f6514eccb4cf8e0678962c2"}, +] +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"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pygments = [ + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, +] +pyparsing = [ + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytz = [ + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, +] +regex = [ + {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, + {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, + {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, + {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, + {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, + {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, + {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, + {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, + {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, + {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, + {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, + {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, + {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, + {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, + {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, + {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, + {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, + {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, + {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +sortedcontainers = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] +sphinx = [ + {file = "Sphinx-4.3.1-py3-none-any.whl", hash = "sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f"}, + {file = "Sphinx-4.3.1.tar.gz", hash = "sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, +] +typed-ast = [ + {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, + {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, + {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, + {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, + {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, + {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, + {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, + {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, + {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, + {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, + {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, + {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, + {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, + {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, +] +typing-extensions = [ + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, +] +typing-inspect = [ + {file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"}, + {file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"}, + {file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"}, +] +urllib3 = [ + {file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"}, + {file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"}, +] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/pybedlite/__init__.py b/pybedlite/__init__.py new file mode 100644 index 0000000..3cc8294 --- /dev/null +++ b/pybedlite/__init__.py @@ -0,0 +1,93 @@ +""" +Lightweight interfaces for reading and writing BED records +---------------------------------------------------------- + +Examples of Parsing BED files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + >>> import pybedlite as pybed + >>> from pathlib import Path + >>> with pybed.reader(path=Path("infile.bed")) as in_fh: + for record in in_fh: + # Do work with records + pass + +Examples of Writing BED files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + >>> import pybedlite as pybed + >>> from pathlib import Path + >>> # Get records from somewhere + >>> records = [] + >>> with pybed.reader(path=Path("infile.bed)) as in_fh: + for record in in_fh: + records.append(record) + >>> # Write records to somewhere + >>> with pybed.writer(path=Path("outfile.bed"), num_fields=6) as out_fh: + for record in records: + out_fh.write(record) + + +Module Contents +~~~~~~~~~~~~~~~ + +The module contains the following public classes: + + - :class:`~pybedlite.bed_record.BedStrand` -- Enumeration of possible strands for a bed record + - :class:`~pybedlite.bed_record.BedRecord` -- Lightweight class for storing information + pertaining to a BED record. + - :class:`~pybedlite.bed_source.BedSource` -- Reader class for parsing BED files and iterate + over their contained records + - :class:`~pybedlite.bed_writer.BedWriter` -- Writer class for writing BED files + +The module contains the following methods: + + - :func:`pybedlite.reader` -- opens a BED file for reading. + - :func:`pybedlite.writer` -- opens a BED file for writing. + +""" + +from typing import Optional + +from pybedlite.bed_writer import BedWriter +from pybedlite.bed_source import BedSource +from pybedlite.bed_source import BedPath +from pybedlite.bed_record import BedRecord +from pybedlite.bed_record import BedStrand + + +def reader(path: BedPath) -> "BedSource": + """Returns BED file for reading. File handle will need to be opened with the + + Args: + path: a file handle or path to the Bed to read. + """ + return BedSource(path=path) + + +def writer( + path: BedPath, + num_fields: Optional[int] = None, +) -> "BedWriter": + """Returns a BED file for writing. + + Args: + path: a file handle or path to the BED to write. + num_fields: the number of BED fields to write for each record. If none this value will be + set to the number of fields present in the first BED record written by this object. + """ + return BedWriter(path=path, num_fields=num_fields) + + +__all__ = [ + "reader", + "writer", + "BedSource", + "BedWriter", + "BedRecord", + "BedStrand", +] diff --git a/pybedlite/bed_record.py b/pybedlite/bed_record.py new file mode 100644 index 0000000..0ba692f --- /dev/null +++ b/pybedlite/bed_record.py @@ -0,0 +1,190 @@ +""" +Lightweight interface for reading and writing BED records +--------------------------------------------------------- + +Module Contents +~~~~~~~~~~~~~~~ + +The module contains the following public classes: + + - :class:`~pybedtools.bed_record.BedStrand` -- Enumeration of possible strands for a bed record + - :class:`~pybedtools.bed_record.BedRecord` -- Lightweight class for storing information + pertaining to a BED record. +""" +import attr +import enum +from typing import Optional +from typing import Tuple +from typing import List +from typing import ClassVar + + +"""Maximum BED fields that can be present in a well formed BED file written to specification""" +MAX_BED_FIELDS: int = 12 + + +@enum.unique +class BedStrand(enum.Enum): + Positive = "+" + Negative = "-" + + +@attr.s(frozen=True, auto_attribs=True, kw_only=True, slots=True) +class BedRecord: + """Lightweight class for storing BED records. A more comprehensive description of BED format + can be found at https://genome.ucsc.edu/FAQ/FAQformat.html#format1. Only `chrom`, `start`, and + `end` are required. + + Attributes: + chrom: the reference name of the interval described by the record + start: the start coordinate in 0-based half open coordinates (inclusive) + end: the end coordinate in 0-based half open coordinates (exclusive) + name: the name of the bed record + score: a score for the interval (Officially in the UCSC spec should be between 0 and 1000, + however we do not enforce this constraint on the score. We only require that if + defined it stores an integer) + strand: defines the strand of the interval described by the record + thick_start: the starting position at which the bed feature should be drawn thickly + thick_end: the ending position at which the bed feature should be drawn thickly + item_rgb: an RGB value of the form (R, G, B) + block_count: the number of blocks in the bed line + block_sizes: a list of block (exon) sizes. Number of items must correspond to + block_count + block_starts: a list of block (exon) starts relative to start. 0-based inclusive. + The number of items must correspond to block_count + """ + + chrom: str + start: int = attr.ib() + end: int + name: Optional[str] = None + score: Optional[int] = None + strand: Optional[BedStrand] = None + thick_start: Optional[int] = attr.ib(default=None) + thick_end: Optional[int] = None + item_rgb: Optional[Tuple[int, int, int]] = None + block_count: Optional[int] = attr.ib(default=None) + block_sizes: Optional[List[int]] = None + block_starts: Optional[List[int]] = attr.ib(default=None) + + MissingValue: ClassVar[str] = "." + + ############## + # Validators # + ############## + + @start.validator + def _validate_start(self, attribute: str, value: Optional[int]) -> None: + assert self.end > value, ( + "End of interval must be greater than start of interval." + + f" start: {value}, end: {self.end}" + ) + + @thick_start.validator + def _validate_thick_definitions(self, attribute: str, value: Optional[int]) -> None: + if value is None: + assert self.thick_end is None, "Thick end cannot be defined if thick start is not" + else: + assert self.thick_end is not None, "Thick start cannot be defined if thick end is not" + + @block_count.validator + def _validate_block_counts(self, attribute: str, value: Optional[int]) -> None: + if value is None: + assert ( + self.block_sizes is None + ), "Block count must be defined if block sizes is defined" + assert ( + self.block_starts is None + ), "Block count must be defined if block starts is defined" + else: + assert ( + self.block_sizes is not None + ), "Block sizes cannot be undefined if block count is defined" + assert ( + self.block_starts is not None + ), "Block starts cannot be undefined if block count is defined" + assert ( + len(self.block_sizes) == value + ), "Number of items in block_sizes must match block_counts" + assert ( + len(self.block_starts) == value + ), "Number of items in block_starts must match block_counts" + + @block_starts.validator + def _validate_bounds(self, attribute: str, value: Optional[List[int]]) -> None: + if value is not None: + assert 0 == value[0], "Block start at first position should be zero" + assert self.end == self.start + value[-1] + self.block_sizes[-1], ( + "overall interval end should be equal to the last defined block's end. " + + f"Last block end: {self.start + value[-1] + self.block_sizes[-1]}, " + + f"Interval end: {self.end}" + ) + + @property + def bed_field_num(self) -> int: + if self.block_starts is not None: + return 12 + elif self.item_rgb is not None: + return 9 + elif self.thick_end is not None: + return 8 + elif self.strand is not None: + return 6 + elif self.score is not None: + return 5 + elif self.name is not None: + return 4 + else: + return 3 + + @property + def bed_fields(self) -> List[str]: + """ + Converts a BED record to a list of it's BED field string equivalents + """ + return [ + self.chrom, + f"{self.start}", + f"{self.end}", + BedRecord.MissingValue if self.name is None else self.name, + BedRecord.MissingValue if self.score is None else f"{self.score}", + BedRecord.MissingValue if self.strand is None else self.strand.value, + BedRecord.MissingValue if self.thick_start is None else f"{self.thick_start}", + BedRecord.MissingValue if self.thick_end is None else f"{self.thick_end}", + ( + BedRecord.MissingValue + if self.item_rgb is None + else ",".join([f"{x}" for x in self.item_rgb]) + ), + BedRecord.MissingValue if self.block_count is None else f"{self.block_count}", + ( + BedRecord.MissingValue + if self.block_sizes is None + else ",".join([f"{x}" for x in self.block_sizes]) + ), + ( + BedRecord.MissingValue + if self.block_starts is None + else ",".join([f"{x}" for x in self.block_starts]) + ), + ] + + def as_bed_line(self, number_of_output_fields: Optional[int] = None) -> str: + """ + Converts a BED record to a tab delimited BED line equivalent, including up to the number of + fields specified in the output line. + + Args: + number_of_output_fields: the number of fields that should be output in the bed line. + i.e. if you'd like a BED6 line, this should be set to 6. Etc. + """ + + assert ( + number_of_output_fields is None or 3 <= number_of_output_fields <= MAX_BED_FIELDS + ), "BED records can only contain between 3 and 12 fields" + + number_of_output_fields = ( + self.bed_field_num if number_of_output_fields is None else number_of_output_fields + ) + fields = self.bed_fields[:number_of_output_fields] + return "\t".join(fields) diff --git a/pybedlite/bed_source.py b/pybedlite/bed_source.py new file mode 100644 index 0000000..aa62dc7 --- /dev/null +++ b/pybedlite/bed_source.py @@ -0,0 +1,165 @@ +""" +Reader class for BED files producing BedRecords +----------------------------------------------- + +Module Contents +~~~~~~~~~~~~~~~ + +The module contains the following public classes: + + - :class:`~pybedtools.bed_source.BedSource` -- Reader class for parsing BED files and iterate + over their contained records +""" +import io +from typing import IO +from typing import Optional +from typing import TypeVar +from typing import Union +from typing import Type +from typing import Tuple +from typing import List +from pathlib import Path +from types import TracebackType +from typing import Callable +from typing import ContextManager +from typing import Iterable +from typing import Iterator +from typing import Dict +from typing import Any + +from pybedlite.bed_record import BedStrand +from pybedlite.bed_record import BedRecord + +"""The classes that should be treated as file-like classes""" +_IOClasses = (io.TextIOBase, io.BufferedIOBase, io.RawIOBase, io.IOBase) + +"""The valid base classes for opening a BED file.""" +BedPath = Union[Path, str, IO[Any]] + +T = TypeVar("T") + + +class BedSource(ContextManager, Iterable[BedRecord]): + """Reader for BED records stored in a BED file + + Attributes: + num_fields: the number of BED fields present for records in this file. This will be set to + the number of fields present in the first record parsed by this class. Note that while + the official BED spec indicates that all BED records in the same file should be written + with the same number of fields, we do not enforce that this is the case in the BED + files this reader parses. + """ + + def __init__(self, path: BedPath) -> None: + self._path: Optional[Path] + self._in_fh: Optional[IO] + self._file_is_open: bool + + if isinstance(path, (str, Path)): + self._path = Path(path) + self._in_fh = None + self._file_is_open = False + elif isinstance(path, _IOClasses): + self._path = None + self._in_fh = path + self._file_is_open = not self._in_fh.closed + else: + raise TypeError(f"Cannot open '{type(path)}' for reading.") + + self.num_fields: Optional[int] = None + + def __enter__(self) -> "BedSource": + return self.open() + + def __exit__( + self, + __exc_type: Type[BaseException], + __exc_value: BaseException, + __traceback: TracebackType, + ) -> None: + self.close() + + def open(self) -> "BedSource": + """Opens the BedSources file for reading. Must be called before iterating over the file. + Make sure to close when done. + """ + if self._in_fh is None or (not self._file_is_open and self._path is not None): + self._in_fh = self._path.open("r") + self._file_is_open = True + else: + assert self._file_is_open, "File must be pre-opened if filehandle specified" + return self + + def close(self) -> None: + """Closes the BedSources file. Should be called after iterating over the file.""" + assert self._file_is_open, f"Cannot close file {self._path} if it is not already open!" + self._file_is_open = False + self._in_fh.close() + + def __iter__(self) -> Iterator[BedRecord]: + def helper(fields: List[str], index: int, present_fn: Callable[[str], T]) -> Optional[T]: + if len(fields) <= index or fields[index] == BedRecord.MissingValue: + return None + return present_fn(fields[index]) + + def parse_rgb(x: str) -> Tuple[int, int, int]: + # This is fairly verbose for what it's doing, but it makes mypy happy because + # if converted to a tuple directly there's no bounds on its size. + rgb_split = [int(x) for x in fields[8].split(",")] + assert ( + len(rgb_split) == 3 + ), "item_rgb, if defined, must contain 3 comma separated integers" + r, g, b = rgb_split + return (r, g, b) + + context_managed_by_iterator: bool = False + if not self._file_is_open: + context_managed_by_iterator = True + self.open() + self._file_is_open = True + + for i, line in enumerate(self._in_fh): + # Skip header lines + if line.startswith("#") or line.startswith("browser") or line.startswith("track"): + continue + fields = line.strip().split("\t") + assert len(fields) >= 3, ( + "BED records must conform to specifications, which requires at least 3 input " + + f"fields. On line {i} in {self._path} had only {len(fields)} fields" + ) + + num_fields = len(fields) + + if self.num_fields is None: + self.num_fields = num_fields + + init_args: Dict[str, Any] = {} + + init_args["chrom"] = fields[0] + init_args["start"] = int(fields[1]) + init_args["end"] = int(fields[2]) + if num_fields >= 10: + init_args["block_count"] = helper(fields=fields, index=9, present_fn=int) + init_args["block_sizes"] = helper( + fields=fields, index=10, present_fn=lambda x: [int(y) for y in x.split(",")] + ) + init_args["block_starts"] = helper( + fields=fields, index=11, present_fn=lambda x: [int(y) for y in x.split(",")] + ) + if num_fields >= 9: + init_args["item_rgb"] = helper(fields=fields, index=8, present_fn=parse_rgb) + if num_fields >= 7: + init_args["thick_start"] = helper(fields=fields, index=6, present_fn=int) + init_args["thick_end"] = helper(fields=fields, index=7, present_fn=int) + if num_fields >= 6: + init_args["strand"] = helper(fields=fields, index=5, present_fn=BedStrand) + if num_fields >= 5: + init_args["score"] = helper(fields=fields, index=4, present_fn=int) + if num_fields >= 4: + init_args["name"] = helper(fields=fields, index=3, present_fn=lambda x: x) + + yield BedRecord(**init_args) + + if context_managed_by_iterator: + self.close() + self._file_is_open = False diff --git a/pybedlite/bed_writer.py b/pybedlite/bed_writer.py new file mode 100644 index 0000000..d6c9c9a --- /dev/null +++ b/pybedlite/bed_writer.py @@ -0,0 +1,144 @@ +""" +Writer class for outputting BedRecords to a file +------------------------------------------------ + +Module Contents +~~~~~~~~~~~~~~~ + +The module contains the following public classes: + + - :class:`~pybedtools.bed_source.BedWriter` -- Writer class for writing BED files +""" + +from typing import IO +from typing import Optional +from typing import Type +from pathlib import Path +from types import TracebackType +from typing import ContextManager +from typing import Iterable + +from pybedlite.bed_record import BedRecord +from pybedlite.bed_source import BedPath +from pybedlite.bed_source import _IOClasses + + +"""Maximum BED fields that can be present in a well formed BED file written to specification""" +MAX_BED_FIELDS: int = 12 + + +class BedWriter(ContextManager): + """Writer class for writing BED records to a file. + + Attributes: + num_fields: The number of BED fields to report. Must be between 3 and 12. + """ + + def __init__( + self, + path: BedPath, + num_fields: Optional[int] = None, + ) -> None: + """Instantiates a BedWriter. + + Args: + outfile: Path specifying where to write the BED file output by this class + num_fields: Number of BED columns to write in BED file + """ + assert num_fields is None or ( + num_fields >= 3 and num_fields <= MAX_BED_FIELDS + ), "BED files must contain between 3 and 12 columns" + + self._path: Optional[Path] + self._file_handle: Optional[IO] + self._file_is_open: bool + if isinstance(path, (str, Path)): + self._path = Path(path) + self._file_handle = None + self._file_is_open = False + elif isinstance(path, _IOClasses): + self._path = None + self._file_handle = path + self._file_is_open = not self._file_handle.closed + self.num_fields: int = num_fields + + def __enter__(self) -> "BedWriter": + return self.open() + + def __exit__( + self, + __exc_type: Type[BaseException], + __exc_value: BaseException, + __traceback: TracebackType, + ) -> None: + self.close() + + def open(self) -> "BedWriter": + """Opens the BedWriter's file handle.""" + + if self._file_handle is None or (not self._file_is_open and self._path is not None): + self._file_handle = self._path.open("w") + self._file_is_open = True + else: + assert self._file_is_open, "File must be pre-opened if filehandle specified" + return self + + def close(self) -> None: + """Closes the BedWriter file. Should be called after all records to write have been + added. + """ + + assert self._file_is_open, f"Cannot close file {self._path} if it is not already open!" + self._file_is_open = False + self._file_handle.close() + + def write(self, record: BedRecord, truncate: bool = False, add_missing: bool = False) -> None: + """Writes a single BedRecord to the file. + + Args: + record: the BED record to write to the file. + truncate: if false and a BED record is passed with more fields than the writer is set + to output a `ValueError` will be raised. If true such a record will be written in a + truncated fashion, with only the number of fields written by this writer. + add_missing: if false and a BED record is passed with fewer fields than the writer is + set to output a `ValueError` will be raised. If true such a record will be written + in a padded fashion, with '.' output for the missing fields up to the number of + fields written by this writer. + """ + if self.num_fields is None: + self.num_fields = record.bed_field_num + + if self.num_fields < record.bed_field_num and not truncate: + raise ValueError( + "To write a record to a BED file with fewer BED fields than are present in the " + + "record truncate must be set to True. " + + f"number of fields expected {self.num_fields}, " + + f"number of fields observed: {record.bed_field_num}" + ) + elif self.num_fields > record.bed_field_num and not add_missing: + raise ValueError( + "To write a record to a BED file with more fields than are present in the " + + "record add_missing must be set to True. " + + f"number of fields expected {self.num_fields}, " + + f"number of fields observed: {record.bed_field_num}" + ) + + self._file_handle.write(f"{record.as_bed_line(number_of_output_fields=self.num_fields)}\n") + + def write_all( + self, records: Iterable[BedRecord], truncate: bool = False, add_missing: bool = False + ) -> None: + """Writes multiple BedRecords to a file + + Arguments: + records: the BED records to write to the file (must be iterable) + truncate: if false and a BED record is passed with more fields than the writer is set + to output a `ValueError` will be raised. If true such records will be written in a + truncated fashion, with only the number of fields written by this writer + add_missing: if false and a BED record is passed with fewer fields than the writer is + set to output a `ValueError` will be raised. If true such records will be written + in a padded fashion, with '.' output for the missing fields up to the number of + fields written by this writer. + """ + for record in records: + self.write(record, truncate=truncate, add_missing=add_missing) diff --git a/pybedlite/overlap_detector.py b/pybedlite/overlap_detector.py new file mode 100644 index 0000000..759882e --- /dev/null +++ b/pybedlite/overlap_detector.py @@ -0,0 +1,224 @@ +""" +Utility Classes for Querying Overlaps with Genomic Regions +---------------------------------------------------------- + +Examples of Detecting Overlaps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + >>> from pybedlite.overlap_detector import Interval, OverlapDetector + >>> detector = OverlapDetector() + >>> query = Interval("chr1", 2, 20) + >>> detector.overlaps_any(query) + False + >>> detector.add(Interval("chr2", 1, 100)) + >>> detector.add(Interval("chr1", 21, 100)) + >>> detector.overlaps_any(query) + False + >>> detector.add(Interval("chr1", 1, 1)) + >>> detector.overlaps_any(query) + True + >>> detector.get_overlaps(query) + [Interval("chr1", 1, 1)] + >>> detector.add(Interval("chr1", 3, 10)) + >>> detector.overlaps_any(query) + True + >>> detector.get_overlaps(query) + [Interval("chr1", 1, 1), interval("chr1", 3, 10)] + +Module Contents +~~~~~~~~~~~~~~~ + +The module contains the following public classes: + + - :class:`~pybedlite.overlap_detector.Interval` -- Represents a region mapping to the genome + that is 0-based and open-ended + - :class:`~pybedlite.overlap_detector.OverlapDetector` -- Detects and returns overlaps between + a set of genomic regions and another genomic region +""" + +from pathlib import Path +from typing import Dict +from typing import Iterable +from typing import List +from typing import Optional + +import attr +from intervaltree import IntervalTree +from pybedlite.bed_source import BedSource +from pybedlite.bed_record import BedStrand + + +@attr.s(frozen=True, auto_attribs=True) +class Interval: + """A region mapping to the genome that is 0-based and open-ended + + Attributes: + refname (str): the refname (or chromosome) + start (int): the 0-based start position + end (int): the 0-based end position (exclusive) + negative (bool): true if the interval is on the negative strand, false otherwise + name (Optional[str]): an optional name assigned to the interval + """ + + refname: str = attr.ib() + start: int = attr.ib() + end: int = attr.ib() + negative: bool = False + name: Optional[str] = None + + def __attrs_post_init__(self) -> None: + """Performs simple validation. + + Checks: + - 0 <= start + - start < end + """ + if self.start < 0: + raise ValueError(f"start is out of range: {self.start}") + if self.end <= self.start: + raise ValueError(f"end < start: {self.end} < {self.start}") + + def overlap(self, other: "Interval") -> int: + """Returns the overlap between this interval and the other, or zero if there is none. + + Args: + other (Interval): the other interval to find the overlap with + """ + if self.refname != other.refname: + return 0 + + overlap = min(self.end, other.end) - max(self.start, other.start) + return overlap if overlap > 0 else 0 + + def length(self) -> int: + """Returns the length of the interval.""" + return self.end - self.start + + +class OverlapDetector: + """Detects and returns overlaps between a set of genomic regions and another genomic region. + + If two intervals have the same coordinates, only the first that was added will be kept. + + Since :class:`~samwell.overlap_detector.Interval` objects are used both to populate the + overlap detector and to query it, the coordinate system in use is also 0-based open-ended. + """ + + def __init__(self) -> None: + self._refname_to_tree: Dict[str, IntervalTree] = {} + + def add(self, interval: Interval) -> None: + """Adds a interval to this detector. + + Args: + interval: the interval to add to this detector + """ + refname = interval.refname + if refname not in self._refname_to_tree: + self._refname_to_tree[refname] = IntervalTree() + tree = self._get_tree(refname) + tree[interval.start : interval.end] = interval + + def add_all(self, intervals: Iterable[Interval]) -> None: + """Adds one or more intervals to this detector. + + Args: + intervals: the intervals to add to this detector + """ + for interval in intervals: + self.add(interval) + + def overlaps_any(self, interval: Interval) -> bool: + """Determines whether the given interval overlaps any interval in this detector. + + Args: + interval: the interval to check + + Returns: + True if and only if the given interval overlaps with any interval in this + detector. + """ + tree = self._get_tree(interval.refname) + if tree is None: + return False + else: + return tree.overlaps(interval.start, interval.end) + + def get_overlaps(self, interval: Interval) -> List[Interval]: + """Returns any intervals in this detector that overlap the given interval. + + Args: + interval: the interval to check + + Returns: + The list of intervals in this detector that overlap the given interval, or the empty + list if no overlaps exist. The intervals will be return in ascending genomic order. + """ + tree = self._get_tree(interval.refname) + if tree is None: + return [] + else: + intervals = [i.data for i in tree.overlap(interval.start, interval.end)] + return sorted(intervals, key=lambda intv: (intv.start, intv.end)) + + def get_enclosing_intervals(self, interval: Interval) -> List[Interval]: + """Returns the set of intervals in this detector that wholly enclose the query interval. + i.e. query.start >= target.start and query.end <= target.end. + + Args: + interval: the query interval + Returns: + The list of intervals in this detector that enclose the query interval. + The intervals will be returned in ascending genomic order. + """ + results = self.get_overlaps(interval) + return [i for i in results if interval.start >= i.start and interval.end <= i.end] + + def get_enclosed(self, interval: Interval) -> List[Interval]: + """Returns the set of intervals in this detector that are enclosed by the query + interval. I.e. target.start >= query.start and target.end <= query.end. + + Args: + interval: the query interval + + Returns: + The list of intervals in this detector that are enclosed within the query interval. + The intervals will be return in ascending genomic order. + """ + results = self.get_overlaps(interval) + return [i for i in results if i.start >= interval.start and i.end <= interval.end] + + def _get_tree(self, refname: str) -> Optional[IntervalTree]: + """Gets the detector for the given refname + + Args: + refname: the refname + + Returns: + the detector for the given refname, or None if there are no regions for that refname + """ + return self._refname_to_tree.get(refname) + + @classmethod + def from_bed(cls, path: Path) -> "OverlapDetector": + """Builds an :class:`~samwell.overlap_detector.OverlapDetector` from a BED file. + + Args: + path: the path to the BED file + + Returns: + An overlap detector for the regions in the BED file. + """ + detector = OverlapDetector() + for region in BedSource(path): + locatable = Interval( + refname=region.chrom, + start=region.start, + end=region.end, + negative=region.strand == BedStrand.Negative, + name=region.name, + ) + detector.add(locatable) + return detector diff --git a/pybedlite/tests/__init__.py b/pybedlite/tests/__init__.py new file mode 100644 index 0000000..cd91c84 --- /dev/null +++ b/pybedlite/tests/__init__.py @@ -0,0 +1 @@ +# This is here so mypy can find our files diff --git a/pybedlite/tests/test_overlap_detector.py b/pybedlite/tests/test_overlap_detector.py new file mode 100644 index 0000000..93a117a --- /dev/null +++ b/pybedlite/tests/test_overlap_detector.py @@ -0,0 +1,136 @@ +"""Tests for :py:mod:`~pybedlite.overlap_detector`""" + +from typing import List + +from pybedlite.overlap_detector import Interval +from pybedlite.overlap_detector import OverlapDetector + + +def run_test(targets: List[Interval], query: Interval, results: List[Interval]) -> None: + detector = OverlapDetector() + # Use add_all() to covert itself and add() + detector.add_all(intervals=targets) + # Test overlaps_any() + assert detector.overlaps_any(query) == (len(results) > 0) + # Test get_overlaps() + assert detector.get_overlaps(query) == results + + +def test_same_interval() -> None: + interval = Interval("1", 10, 100) + run_test(targets=[interval], query=interval, results=[interval]) + + +def test_query_wholly_contained_in_target() -> None: + target = Interval("1", 10, 100) + query = Interval("1", 11, 99) + run_test(targets=[target], query=query, results=[target]) + + +def test_target_wholly_contained_in_query() -> None: + target = Interval("1", 10, 100) + query = Interval("1", 9, 101) + run_test(targets=[target], query=query, results=[target]) + + +def test_target_overlaps_first_base_of_query() -> None: + target = Interval("1", 10, 100) + query = Interval("1", 99, 100) + run_test(targets=[target], query=query, results=[target]) + + +def test_target_overlaps_last_base_of_query() -> None: + target = Interval("1", 10, 100) + query = Interval("1", 10, 11) + run_test(targets=[target], query=query, results=[target]) + + +def test_query_before_target() -> None: + target = Interval("1", 10, 100) + query = Interval("1", 9, 10) + run_test(targets=[target], query=query, results=[]) + + +def test_query_after_target() -> None: + target = Interval("1", 10, 100) + query = Interval("1", 100, 101) + run_test(targets=[target], query=query, results=[]) + + +def test_different_references() -> None: + target = Interval("1", 10, 100) + query = Interval("2", 10, 100) + run_test(targets=[target], query=query, results=[]) + + +def test_multiple_overlaps() -> None: + interval_a = Interval("1", 10, 20) + interval_b = Interval("1", 15, 25) + interval_c = Interval("1", 19, 30) + interval_d = Interval("1", 24, 35) + + # B overlaps both A and C + run_test(targets=[interval_a, interval_c], query=interval_b, results=[interval_a, interval_c]) + # C overlaps both A and B + run_test(targets=[interval_a, interval_b], query=interval_c, results=[interval_a, interval_b]) + # D overlaps only B and C (is after A) + run_test( + targets=[interval_a, interval_b, interval_c], + query=interval_d, + results=[interval_b, interval_c], + ) + + +def test_multiple_references() -> None: + target_chr1 = Interval("1", 10, 20) + target_chr2 = Interval("2", 10, 20) + run_test(targets=[target_chr1, target_chr2], query=target_chr1, results=[target_chr1]) + run_test(targets=[target_chr1, target_chr2], query=target_chr2, results=[target_chr2]) + + +def test_same_interval_twice() -> None: + interval = Interval("1", 10, 100) + run_test(targets=[interval, interval], query=interval, results=[interval]) + + +def test_wholly_contained_target() -> None: + target_inner = Interval("1", 50, 60) + target_outer = Interval("1", 40, 80) + + run_test( + targets=[target_inner, target_outer], + query=target_inner, + results=[target_outer, target_inner], + ) + + +def test_get_enclosing_intervals() -> None: + a = Interval("1", 1, 250) + b = Interval("1", 5, 30) + c = Interval("1", 10, 99) + d = Interval("1", 15, 19) + e = Interval("1", 16, 20) + + detector = OverlapDetector() + detector.add_all([a, b, c, d, e]) + + assert detector.get_enclosing_intervals(Interval("1", 10, 100)) == [a] + assert detector.get_enclosing_intervals(Interval("1", 15, 20)) == [a, b, c] + assert detector.get_enclosing_intervals(Interval("1", 18, 19)) == [a, b, c, d, e] + assert detector.get_enclosing_intervals(Interval("1", 50, 99)) == [a, c] + + +def test_get_enclosed() -> None: + a = Interval("1", 10, 100) + b = Interval("1", 15, 20) + c = Interval("1", 18, 19) + d = Interval("1", 50, 99) + + detector = OverlapDetector() + detector.add_all([a, b, c, d]) + + assert detector.get_enclosed(Interval("1", 1, 250)) == [a, b, c, d] + assert detector.get_enclosed(Interval("1", 5, 30)) == [b, c] + assert detector.get_enclosed(Interval("1", 16, 20)) == [c] + assert detector.get_enclosed(Interval("1", 15, 19)) == [c] + assert detector.get_enclosed(Interval("1", 10, 99)) == [b, c, d] diff --git a/pybedlite/tests/test_pybedlite.py b/pybedlite/tests/test_pybedlite.py new file mode 100644 index 0000000..267342c --- /dev/null +++ b/pybedlite/tests/test_pybedlite.py @@ -0,0 +1,256 @@ +"""Tests for :py:mod:`~pybedlite.__init__.py`""" + +import pytest +from pathlib import Path +from py._path.local import LocalPath as TmpDir +from typing import List +import pybedlite as pybed +from pybedlite.bed_writer import MAX_BED_FIELDS +from pybedlite.bed_writer import BedWriter +from pybedlite.bed_source import BedSource +from pybedlite.bed_record import BedRecord +from pybedlite.bed_record import BedStrand + + +@pytest.fixture +def bed_records() -> List[BedRecord]: + return [ + BedRecord( + chrom="1", + start=100, + end=150, + name="test_record1", + score=100, + strand=BedStrand.Positive, + thick_start=100, + thick_end=100, + item_rgb=(0, 0, 0), + block_count=1, + block_sizes=[50], + block_starts=[0], + ), + BedRecord( + chrom="1", + start=200, + end=300, + name="test_record2", + score=100, + strand=BedStrand.Negative, + thick_start=210, + thick_end=290, + item_rgb=(0, 0, 0), + block_count=1, + block_sizes=[100], + block_starts=[0], + ), + BedRecord( + chrom="2", + start=200, + end=300, + name="test_record3", + score=None, + strand=None, + thick_start=None, + thick_end=None, + item_rgb=None, + block_count=None, + block_sizes=None, + block_starts=None, + ), + ] + + +SNIPPET_BED = """\ +1 100 150 test_record1 100 + 100 100 0,0,0 1 50 0 +1 200 300 test_record2 100 - 210 290 0,0,0 1 100 0 +2 200 300 test_record3 . . . . . . . . +""" + + +def compare_bed_records( + record1: BedRecord, + record2: BedRecord, + record_number: int, + num_fields: int = MAX_BED_FIELDS, +) -> None: + assert record1.chrom == record2.chrom, f"Chromosome didn't match in record {record_number}" + assert ( + record1.start == record2.start + ), f"Start coordinate didn't match in record {record_number}" + assert record1.end == record2.end, f"End coordinate didn't match in record {record_number}" + if num_fields >= 4: + assert record1.name == record2.name, f"Name didn't match in record {record_number}" + if num_fields >= 5: + assert record1.score == record2.score, f"Score didn't match in record {record_number}" + if num_fields >= 6: + assert record1.strand == record2.strand, f"Strand didn't match in record {record_number}" + if num_fields >= 7: + assert ( + record1.thick_start == record2.thick_start + ), f"Thick start didn't match in record {record_number}" + assert ( + record1.thick_end == record2.thick_end + ), f"Thick end didn't match in record {record_number}" + if num_fields >= 9: + assert ( + record1.item_rgb == record2.item_rgb + ), f"Item RGB didn't match in record {record_number}" + if num_fields >= 10: + assert ( + record1.block_count == record2.block_count + ), f"Block count didn't match in record {record_number}" + assert ( + record1.block_sizes == record2.block_sizes + ), f"Block sizes didn't match in record {record_number}" + assert ( + record1.block_starts == record2.block_starts + ), f"Block starts didn't match in record {record_number}" + + assert record1.as_bed_line(num_fields) == record2.as_bed_line( + num_fields + ), f"Derived bed lines differed from expectation in record {record_number}" + + +@pytest.mark.parametrize( + "bed_field_number", + [ + 3, + 4, + 5, + 6, + 8, + 9, + 12, + ], +) +def test_bed_parsing(tmpdir: TmpDir, bed_field_number: int, bed_records: List[BedRecord]) -> None: + tmpdir_path = Path(tmpdir) + test_bed = tmpdir_path / "test.bed" + + with BedWriter(test_bed, num_fields=bed_field_number) as test_out: + test_out.write_all(bed_records, truncate=True, add_missing=True) + + with BedSource(test_bed) as test_in: + for i, parsed_record in enumerate(test_in): + expected_record = bed_records[i] + compare_bed_records( + record1=parsed_record, + record2=expected_record, + record_number=i, + num_fields=bed_field_number, + ) + + +@pytest.mark.parametrize( + "bed_field_number", + [ + 3, + 4, + 5, + 6, + 8, + 9, + 12, + ], +) +def test_preopened_bed_parsing( + tmpdir: TmpDir, bed_field_number: int, bed_records: List[BedRecord] +) -> None: + tmpdir_path = Path(tmpdir) + test_bed = tmpdir_path / "test.bed" + + with BedWriter(test_bed, num_fields=bed_field_number) as test_out: + test_out.write_all(bed_records, truncate=True, add_missing=True) + + test_bed_fh = test_bed.open("r") + + with pybed.reader(path=test_bed_fh) as test_in: + for i, parsed_record in enumerate(test_in): + expected_record = bed_records[i] + compare_bed_records( + record1=parsed_record, + record2=expected_record, + record_number=i, + num_fields=bed_field_number, + ) + + +@pytest.mark.parametrize( + "bed_field_number", + [ + 3, + 4, + 5, + 6, + 8, + 9, + 12, + ], +) +def test_bed_writing(tmpdir: TmpDir, bed_field_number: int, bed_records: List[BedRecord]) -> None: + tmpdir_path = Path(tmpdir) + test_written_bed = tmpdir_path / "test_written.bed" + test_premade_bed = tmpdir_path / "test_premade.bed" + + with BedWriter(test_written_bed, num_fields=bed_field_number) as test_out: + test_out.write_all(bed_records, truncate=True, add_missing=True) + + with test_premade_bed.open("w") as premade_out: + premade_out.write(SNIPPET_BED) + + with BedSource(test_written_bed) as test_written_in, BedSource( + test_premade_bed + ) as test_premade_in: + for i, (parsed_record, expected_record) in enumerate( + zip(test_written_in, test_premade_in) + ): + compare_bed_records( + record1=parsed_record, + record2=expected_record, + record_number=i, + num_fields=bed_field_number, + ) + + +@pytest.mark.parametrize( + "bed_field_number", + [ + 3, + 4, + 5, + 6, + 8, + 9, + 12, + ], +) +def test_preopened_bed_writing( + tmpdir: TmpDir, bed_field_number: int, bed_records: List[BedRecord] +) -> None: + tmpdir_path = Path(tmpdir) + test_written_bed = tmpdir_path / "test_written.bed" + test_premade_bed = tmpdir_path / "test_premade.bed" + + test_written_bed_fh = test_written_bed.open("w") + + with pybed.writer( + path=test_written_bed_fh, + num_fields=bed_field_number, + ) as test_out: + test_out.write_all(bed_records, truncate=True, add_missing=True) + + with test_premade_bed.open("w") as premade_out: + premade_out.write(SNIPPET_BED) + + with BedSource(test_written_bed) as test_written_in, BedSource( + test_premade_bed + ) as test_premade_in: + for i, (parsed_record, expected_record) in enumerate( + zip(test_written_in, test_premade_in) + ): + compare_bed_records( + record1=parsed_record, + record2=expected_record, + record_number=i, + num_fields=bed_field_number, + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c01a59 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[tool.poetry] +name = "pybedlite" +version = "0.0.1" +description = "Python classes for interfacing with bed intervals" +authors = ["Nils Homer", "Tim Fennell", "Nathan Roach"] +license = "MIT" +readme = "README.md" +homepage = "https://github.com/fulcrumgenomics/pybedlite" +repository = "https://github.com/fulcrumgenomics/pybedlite" +keywords = ["bioinformatics"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Software Development :: Documentation", + "Topic :: Software Development :: Libraries :: Python Modules", +] +include = [ + "LICENSE", +] + +[tool.poetry.dependencies] +python = ">=3.6.0" +attrs = ">=19.3.0" +intervaltree = ">=3.1.0" +typing_extensions = { version = ">=3.7.4", python = "<3.8" } # Literal support +typing_inspect = { version = ">=0.3.1", python = "<3.8" } # inspecting types +sphinx = {version = "4.3.1", optional = true} + +[tool.poetry.dev-dependencies] +pytest = ">=5.4.2" +mypy = ">=0.770" +flake8 = ">=4.0.1" +black = ">=19.10b0" +pytest-cov = ">=2.8.1" + +[tool.poetry.extras] +docs = ["sphinx"] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api"