From 1281606ee154aac514b8d3445595b17266abdfa7 Mon Sep 17 00:00:00 2001 From: AndiMajore <andi.majore@googlemail.com> Date: Thu, 30 Jun 2022 18:19:59 +0200 Subject: [PATCH] python_nedrex code (for now) --- python_nedrex | 1 - python_nedrex/.editorconfig | 21 + python_nedrex/.github/ISSUE_TEMPLATE.md | 15 + python_nedrex/.gitignore | 106 ++++ python_nedrex/.travis.yml | 15 + python_nedrex/AUTHORS.rst | 13 + python_nedrex/CONTRIBUTING.rst | 128 +++++ python_nedrex/HISTORY.rst | 8 + python_nedrex/LICENSE | 22 + python_nedrex/MANIFEST.in | 11 + python_nedrex/Makefile | 89 +++ python_nedrex/README.rst | 39 ++ python_nedrex/docs/Makefile | 20 + python_nedrex/docs/authors.rst | 1 + python_nedrex/docs/conf.py | 160 ++++++ python_nedrex/docs/contributing.rst | 1 + python_nedrex/docs/history.rst | 1 + python_nedrex/docs/index.rst | 20 + python_nedrex/docs/installation.rst | 51 ++ python_nedrex/docs/make.bat | 36 ++ python_nedrex/docs/readme.rst | 1 + python_nedrex/docs/usage.rst | 7 + python_nedrex/format.sh | 6 + python_nedrex/pylintrc | 590 +++++++++++++++++++ python_nedrex/python_nedrex/__init__.py | 46 ++ python_nedrex/python_nedrex/bicon.py | 41 ++ python_nedrex/python_nedrex/closeness.py | 33 ++ python_nedrex/python_nedrex/common.py | 96 ++++ python_nedrex/python_nedrex/core.py | 229 ++++++++ python_nedrex/python_nedrex/decorators.py | 24 + python_nedrex/python_nedrex/diamond.py | 41 ++ python_nedrex/python_nedrex/disorder.py | 31 + python_nedrex/python_nedrex/domino.py | 18 + python_nedrex/python_nedrex/exceptions.py | 6 + python_nedrex/python_nedrex/graph.py | 82 +++ python_nedrex/python_nedrex/kpm.py | 27 + python_nedrex/python_nedrex/must.py | 36 ++ python_nedrex/python_nedrex/ppi.py | 28 + python_nedrex/python_nedrex/relations.py | 87 +++ python_nedrex/python_nedrex/robust.py | 45 ++ python_nedrex/python_nedrex/static.py | 31 + python_nedrex/python_nedrex/trustrank.py | 41 ++ python_nedrex/python_nedrex/validation.py | 80 +++ python_nedrex/python_nedrex/variants.py | 160 ++++++ python_nedrex/python_nedrex/vpd.py | 32 ++ python_nedrex/requirements.txt | 5 + python_nedrex/requirements_dev.txt | 12 + python_nedrex/setup.cfg | 20 + python_nedrex/setup.py | 50 ++ python_nedrex/tests/__init__.py | 1 + python_nedrex/tests/test_python_nedrex.py | 664 ++++++++++++++++++++++ python_nedrex/tox.ini | 26 + 52 files changed, 3353 insertions(+), 1 deletion(-) delete mode 160000 python_nedrex create mode 100644 python_nedrex/.editorconfig create mode 100644 python_nedrex/.github/ISSUE_TEMPLATE.md create mode 100644 python_nedrex/.gitignore create mode 100644 python_nedrex/.travis.yml create mode 100644 python_nedrex/AUTHORS.rst create mode 100644 python_nedrex/CONTRIBUTING.rst create mode 100644 python_nedrex/HISTORY.rst create mode 100644 python_nedrex/LICENSE create mode 100644 python_nedrex/MANIFEST.in create mode 100644 python_nedrex/Makefile create mode 100644 python_nedrex/README.rst create mode 100644 python_nedrex/docs/Makefile create mode 100644 python_nedrex/docs/authors.rst create mode 100755 python_nedrex/docs/conf.py create mode 100644 python_nedrex/docs/contributing.rst create mode 100644 python_nedrex/docs/history.rst create mode 100644 python_nedrex/docs/index.rst create mode 100644 python_nedrex/docs/installation.rst create mode 100644 python_nedrex/docs/make.bat create mode 100644 python_nedrex/docs/readme.rst create mode 100644 python_nedrex/docs/usage.rst create mode 100755 python_nedrex/format.sh create mode 100644 python_nedrex/pylintrc create mode 100644 python_nedrex/python_nedrex/__init__.py create mode 100644 python_nedrex/python_nedrex/bicon.py create mode 100644 python_nedrex/python_nedrex/closeness.py create mode 100644 python_nedrex/python_nedrex/common.py create mode 100644 python_nedrex/python_nedrex/core.py create mode 100644 python_nedrex/python_nedrex/decorators.py create mode 100644 python_nedrex/python_nedrex/diamond.py create mode 100644 python_nedrex/python_nedrex/disorder.py create mode 100644 python_nedrex/python_nedrex/domino.py create mode 100644 python_nedrex/python_nedrex/exceptions.py create mode 100644 python_nedrex/python_nedrex/graph.py create mode 100644 python_nedrex/python_nedrex/kpm.py create mode 100644 python_nedrex/python_nedrex/must.py create mode 100644 python_nedrex/python_nedrex/ppi.py create mode 100644 python_nedrex/python_nedrex/relations.py create mode 100644 python_nedrex/python_nedrex/robust.py create mode 100644 python_nedrex/python_nedrex/static.py create mode 100644 python_nedrex/python_nedrex/trustrank.py create mode 100644 python_nedrex/python_nedrex/validation.py create mode 100644 python_nedrex/python_nedrex/variants.py create mode 100644 python_nedrex/python_nedrex/vpd.py create mode 100644 python_nedrex/requirements.txt create mode 100644 python_nedrex/requirements_dev.txt create mode 100644 python_nedrex/setup.cfg create mode 100644 python_nedrex/setup.py create mode 100644 python_nedrex/tests/__init__.py create mode 100644 python_nedrex/tests/test_python_nedrex.py create mode 100644 python_nedrex/tox.ini diff --git a/python_nedrex b/python_nedrex deleted file mode 160000 index ee1cd32..0000000 --- a/python_nedrex +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ee1cd32fd15f6b73647df70bacb9d0ebd7858236 diff --git a/python_nedrex/.editorconfig b/python_nedrex/.editorconfig new file mode 100644 index 0000000..d4a2c44 --- /dev/null +++ b/python_nedrex/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[Makefile] +indent_style = tab diff --git a/python_nedrex/.github/ISSUE_TEMPLATE.md b/python_nedrex/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..af31396 --- /dev/null +++ b/python_nedrex/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +* python-nedrex version: +* Python version: +* Operating System: + +### Description + +Describe what you were trying to get done. +Tell us what happened, what went wrong, and what you expected to happen. + +### What I Did + +``` +Paste the command(s) you ran and the output. +If there was a crash, please include the traceback here. +``` diff --git a/python_nedrex/.gitignore b/python_nedrex/.gitignore new file mode 100644 index 0000000..4c915d1 --- /dev/null +++ b/python_nedrex/.gitignore @@ -0,0 +1,106 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# IDE settings +.vscode/ +.idea/ diff --git a/python_nedrex/.travis.yml b/python_nedrex/.travis.yml new file mode 100644 index 0000000..aaba487 --- /dev/null +++ b/python_nedrex/.travis.yml @@ -0,0 +1,15 @@ +# Config file for automatic testing at travis-ci.com + +language: python +python: + - 3.8 + - 3.7 + - 3.6 + +# Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors +install: pip install -U tox-travis + +# Command to run tests, e.g. python setup.py test +script: tox + + diff --git a/python_nedrex/AUTHORS.rst b/python_nedrex/AUTHORS.rst new file mode 100644 index 0000000..1dbe264 --- /dev/null +++ b/python_nedrex/AUTHORS.rst @@ -0,0 +1,13 @@ +======= +Credits +======= + +Development Lead +---------------- + +* David James Skelton <james.skelton@newcastle.ac.uk> + +Contributors +------------ + +None yet. Why not be the first? diff --git a/python_nedrex/CONTRIBUTING.rst b/python_nedrex/CONTRIBUTING.rst new file mode 100644 index 0000000..613ac50 --- /dev/null +++ b/python_nedrex/CONTRIBUTING.rst @@ -0,0 +1,128 @@ +.. highlight:: shell + +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/james-skelton/python_nedrex/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +python-nedrex could always use more documentation, whether as part of the +official python-nedrex docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at https://github.com/james-skelton/python_nedrex/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `python_nedrex` for local development. + +1. Fork the `python_nedrex` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/python_nedrex.git + +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + + $ mkvirtualenv python_nedrex + $ cd python_nedrex/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass flake8 and the + tests, including testing other Python versions with tox:: + + $ flake8 python_nedrex tests + $ python setup.py test or pytest + $ tox + + To get flake8 and tox, just pip install them into your virtualenv. + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check + https://travis-ci.com/james-skelton/python_nedrex/pull_requests + and make sure that the tests pass for all supported Python versions. + +Tips +---- + +To run a subset of tests:: + +$ pytest tests.test_python_nedrex + + +Deploying +--------- + +A reminder for the maintainers on how to deploy. +Make sure all your changes are committed (including an entry in HISTORY.rst). +Then run:: + +$ bump2version patch # possible: major / minor / patch +$ git push +$ git push --tags + +Travis will then deploy to PyPI if tests pass. diff --git a/python_nedrex/HISTORY.rst b/python_nedrex/HISTORY.rst new file mode 100644 index 0000000..a00abfc --- /dev/null +++ b/python_nedrex/HISTORY.rst @@ -0,0 +1,8 @@ +======= +History +======= + +0.1.0 (2022-02-23) +------------------ + +* First release on PyPI. diff --git a/python_nedrex/LICENSE b/python_nedrex/LICENSE new file mode 100644 index 0000000..a561325 --- /dev/null +++ b/python_nedrex/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022, David James Skelton + +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/python_nedrex/MANIFEST.in b/python_nedrex/MANIFEST.in new file mode 100644 index 0000000..965b2dd --- /dev/null +++ b/python_nedrex/MANIFEST.in @@ -0,0 +1,11 @@ +include AUTHORS.rst +include CONTRIBUTING.rst +include HISTORY.rst +include LICENSE +include README.rst + +recursive-include tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + +recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif diff --git a/python_nedrex/Makefile b/python_nedrex/Makefile new file mode 100644 index 0000000..054489e --- /dev/null +++ b/python_nedrex/Makefile @@ -0,0 +1,89 @@ +.PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 lint/black +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +lint/flake8: ## check style with flake8 + flake8 python_nedrex tests +lint/black: ## check style with black + black --check python_nedrex tests + +lint: lint/flake8 lint/black ## check style + +test: ## run tests quickly with the default Python + pytest + +test-all: ## run tests on every Python version with tox + tox + +coverage: ## check code coverage quickly with the default Python + coverage run --source python_nedrex -m pytest + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html + +docs: ## generate Sphinx HTML documentation, including API docs + rm -f docs/python_nedrex.rst + rm -f docs/modules.rst + sphinx-apidoc -o docs/ python_nedrex + $(MAKE) -C docs clean + $(MAKE) -C docs html + $(BROWSER) docs/_build/html/index.html + +servedocs: docs ## compile the docs watching for changes + watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + ls -l dist + +install: clean ## install the package to the active Python's site-packages + python setup.py install diff --git a/python_nedrex/README.rst b/python_nedrex/README.rst new file mode 100644 index 0000000..f7946f7 --- /dev/null +++ b/python_nedrex/README.rst @@ -0,0 +1,39 @@ +============= +python-nedrex +============= + + +.. image:: https://img.shields.io/pypi/v/python_nedrex.svg + :target: https://pypi.python.org/pypi/python_nedrex + +.. image:: https://img.shields.io/travis/james-skelton/python_nedrex.svg + :target: https://travis-ci.com/james-skelton/python_nedrex + +.. image:: https://readthedocs.org/projects/python-nedrex/badge/?version=latest + :target: https://python-nedrex.readthedocs.io/en/latest/?version=latest + :alt: Documentation Status + + + + +A Python library for interfacing with the PNeDRex API + + +* Free software: MIT license +* Documentation: https://docs.google.com/document/d/1nUngfKSXkqPi_EPaD9d1w3M0SVAuhj6PY4tRMgA8RMk/edit?usp=sharing +.. + * Documentation: https://python-nedrex.readthedocs.io. + + +Features +-------- + +* TODO + +Credits +------- + +This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. + +.. _Cookiecutter: https://github.com/audreyr/cookiecutter +.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage diff --git a/python_nedrex/docs/Makefile b/python_nedrex/docs/Makefile new file mode 100644 index 0000000..a94a769 --- /dev/null +++ b/python_nedrex/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = python_nedrex +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/python_nedrex/docs/authors.rst b/python_nedrex/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/python_nedrex/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/python_nedrex/docs/conf.py b/python_nedrex/docs/conf.py new file mode 100755 index 0000000..adec9e2 --- /dev/null +++ b/python_nedrex/docs/conf.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# +# python_nedrex documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 13:47:02 2017. +# +# 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. + +# 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. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + +import python_nedrex + +# -- General configuration --------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# 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.viewcode"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "python-nedrex" +copyright = "2022, David James Skelton" +author = "David James Skelton" + +# 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. +# +# The short X.Y version. +version = python_nedrex.__version__ +# The full version, including alpha/beta/rc tags. +release = python_nedrex.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# 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 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 = ["_static"] + + +# -- Options for HTMLHelp output --------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "python_nedrexdoc" + + +# -- Options for LaTeX output ------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "python_nedrex.tex", + "python-nedrex Documentation", + "David James Skelton", + "manual", + ), +] + + +# -- Options for manual page output ------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "python_nedrex", "python-nedrex Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "python_nedrex", + "python-nedrex Documentation", + author, + "python_nedrex", + "One line description of project.", + "Miscellaneous", + ), +] diff --git a/python_nedrex/docs/contributing.rst b/python_nedrex/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/python_nedrex/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/python_nedrex/docs/history.rst b/python_nedrex/docs/history.rst new file mode 100644 index 0000000..2506499 --- /dev/null +++ b/python_nedrex/docs/history.rst @@ -0,0 +1 @@ +.. include:: ../HISTORY.rst diff --git a/python_nedrex/docs/index.rst b/python_nedrex/docs/index.rst new file mode 100644 index 0000000..af528fa --- /dev/null +++ b/python_nedrex/docs/index.rst @@ -0,0 +1,20 @@ +Welcome to python-nedrex's documentation! +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + readme + installation + usage + modules + contributing + authors + history + +Indices and tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/python_nedrex/docs/installation.rst b/python_nedrex/docs/installation.rst new file mode 100644 index 0000000..93c04d4 --- /dev/null +++ b/python_nedrex/docs/installation.rst @@ -0,0 +1,51 @@ +.. highlight:: shell + +============ +Installation +============ + + +Stable release +-------------- + +To install python-nedrex, run this command in your terminal: + +.. code-block:: console + + $ pip install python_nedrex + +This is the preferred method to install python-nedrex, as it will always install the most recent stable release. + +If you don't have `pip`_ installed, this `Python installation guide`_ can guide +you through the process. + +.. _pip: https://pip.pypa.io +.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ + + +From sources +------------ + +The sources for python-nedrex can be downloaded from the `Github repo`_. + +You can either clone the public repository: + +.. code-block:: console + + $ git clone git://github.com/james-skelton/python_nedrex + +Or download the `tarball`_: + +.. code-block:: console + + $ curl -OJL https://github.com/james-skelton/python_nedrex/tarball/master + +Once you have a copy of the source, you can install it with: + +.. code-block:: console + + $ python setup.py install + + +.. _Github repo: https://github.com/james-skelton/python_nedrex +.. _tarball: https://github.com/james-skelton/python_nedrex/tarball/master diff --git a/python_nedrex/docs/make.bat b/python_nedrex/docs/make.bat new file mode 100644 index 0000000..35b3301 --- /dev/null +++ b/python_nedrex/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=python_nedrex + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/python_nedrex/docs/readme.rst b/python_nedrex/docs/readme.rst new file mode 100644 index 0000000..72a3355 --- /dev/null +++ b/python_nedrex/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/python_nedrex/docs/usage.rst b/python_nedrex/docs/usage.rst new file mode 100644 index 0000000..d6a0513 --- /dev/null +++ b/python_nedrex/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +To use python-nedrex in a project:: + + import python_nedrex diff --git a/python_nedrex/format.sh b/python_nedrex/format.sh new file mode 100755 index 0000000..5915b2f --- /dev/null +++ b/python_nedrex/format.sh @@ -0,0 +1,6 @@ +black -l 120 python_nedrex +isort --profile black python_nedrex +flake8 --max-line-length=120 python_nedrex +pylint --max-line-length=120 python_nedrex +bandit python_nedrex +mypy --strict python_nedrex diff --git a/python_nedrex/pylintrc b/python_nedrex/pylintrc new file mode 100644 index 0000000..77a744e --- /dev/null +++ b/python_nedrex/pylintrc @@ -0,0 +1,590 @@ +[MASTER] + +# NOTE: This has been modified to disable C0114, C0115, and C0116 (docstrings) + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. The default value ignores emacs file +# locks +ignore-patterns=^\.# + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.6 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )?<?https?://\S+>?$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/python_nedrex/python_nedrex/__init__.py b/python_nedrex/python_nedrex/__init__.py new file mode 100644 index 0000000..cc7d76f --- /dev/null +++ b/python_nedrex/python_nedrex/__init__.py @@ -0,0 +1,46 @@ +"""Top-level package for python-nedrex.""" + +__author__ = """David James Skelton""" +__email__ = "james.skelton@newcastle.ac.uk" +__version__ = "0.1.1" + +from typing import Optional + +from attrs import define + + +@define +class _Config: + _url_base: Optional[str] = None + _url_vpd: Optional[str] = None + _api_key: Optional[str] = None + + @property + def url_base(self) -> Optional[str]: + """Returns the API URL base stored on the _Config instance""" + return self._url_base + + @property + def url_vpd(self) -> Optional[str]: + """Returns the VPD URL base stored on the _Config instance""" + return self._url_vpd + + @property + def api_key(self) -> Optional[str]: + """Returns the API key stored on the _Config instance""" + return self._api_key + + def set_url_base(self, url_base: str) -> None: + """Sets the URL base for the API in the configuration""" + self._url_base = url_base.rstrip("/") + + def set_url_vpd(self, url_vpd: str) -> None: + """Sets the URL base for the VPD in the configuration""" + self._url_vpd = url_vpd.rstrip("/") + + def set_api_key(self, key: str) -> None: + """Sets the API key in the configuration""" + self._api_key = key + + +config: _Config = _Config() diff --git a/python_nedrex/python_nedrex/bicon.py b/python_nedrex/python_nedrex/bicon.py new file mode 100644 index 0000000..0d2693f --- /dev/null +++ b/python_nedrex/python_nedrex/bicon.py @@ -0,0 +1,41 @@ +from pathlib import Path as _Path +from typing import IO as _IO +from typing import Any as _Any +from typing import Dict as _Dict +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import download_file as _download_file +from python_nedrex.common import http as _http + + +def bicon_request( + expression_file: _IO[str], + lg_min: int = 10, + lg_max: int = 15, + network: str = "DEFAULT", +) -> str: + files = {"expression_file": expression_file} + data = {"lg_min": lg_min, "lg_max": lg_max, "network": network} + + url = f"{_config.url_base}/bicon/submit" + resp = _http.post(url, data=data, files=files, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +def check_bicon_status(uid: str) -> _Dict[str, _Any]: + url = f"{_config.url_base}/bicon/status" + resp = _http.get(url, params={"uid": uid}, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _Any] = _check_response(resp) + return result + + +def download_bicon_data(uid: str, target: _Optional[str] = None) -> None: + if target is None: + target = str(_Path(f"{uid}.zip").resolve()) + + url = f"{_config.url_base}/bicon/download?uid={uid}" + + _download_file(url, target) diff --git a/python_nedrex/python_nedrex/closeness.py b/python_nedrex/python_nedrex/closeness.py new file mode 100644 index 0000000..f75d811 --- /dev/null +++ b/python_nedrex/python_nedrex/closeness.py @@ -0,0 +1,33 @@ +from typing import List as _List +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + + +def closeness_submit( + seeds: _List[str], + only_direct_drugs: bool = True, + only_approved_drugs: bool = True, + N: _Optional[int] = None, # pylint: disable=C0103 +) -> str: + url = f"{_config.url_base}/closeness/submit" + + body = {"seeds": seeds, "only_direct_drugs": only_direct_drugs, "only_approved_drugs": only_approved_drugs, "N": N} + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +check_closeness_status = _check_status_factory("/closeness/status") + + +def download_closeness_results(uid: str) -> str: + url = f"{_config.url_base}/closeness/download" + params = {"uid": uid} + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp, return_type="text") + return result diff --git a/python_nedrex/python_nedrex/common.py b/python_nedrex/python_nedrex/common.py new file mode 100644 index 0000000..5a0adfd --- /dev/null +++ b/python_nedrex/python_nedrex/common.py @@ -0,0 +1,96 @@ +import urllib.request +from typing import Any, Callable, Dict, Optional + +import cachetools +import requests # type: ignore +from requests.adapters import HTTPAdapter # type: ignore +from urllib3.util.retry import Retry # type: ignore + +from python_nedrex import config +from python_nedrex.exceptions import ConfigError, NeDRexError + +# Start - code derived from https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/ +DEFAULT_TIMEOUT = 120 + + +class TimeoutHTTPAdapter(HTTPAdapter): # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.timeout = DEFAULT_TIMEOUT + if "timeout" in kwargs: + self.timeout = kwargs["timeout"] + del kwargs["timeout"] + super().__init__(*args, **kwargs) + + # pylint: disable=arguments-differ + def send(self, request: requests.Request, **kwargs: Any) -> requests.Response: + timeout = kwargs.get("timeout") + if timeout is None: + kwargs["timeout"] = self.timeout + return super().send(request, **kwargs) + + +retry_strategy = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]) + +http = requests.Session() +adapter = TimeoutHTTPAdapter(max_retries=retry_strategy) +http.mount("https://", adapter) +http.mount("http://", adapter) +# End - code derived from https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/ + + +def check_response(resp: requests.Response, return_type: str = "json") -> Any: + if resp.status_code == 401: + data = resp.json() + if data["detail"] == "An API key is required to access the requested data": + raise ConfigError("no API key set in the configuration") + + if resp.status_code == 422: + data = resp.json() + raise NeDRexError(data["detail"]) + + if resp.status_code == 404: + raise NeDRexError("not found") + + if return_type == "json": + data = resp.json() + elif return_type == "text": + data = resp.text + else: + raise NeDRexError(f"invalid value for return_type ({return_type!r}) in check_response") + return data + + +@cachetools.cached(cachetools.TTLCache(1, ttl=10)) +def get_pagination_limit() -> Any: + url = f"{config.url_base}/pagination_max" + return requests.get(url, headers={"x-api-key": config.api_key}).json() + + +def check_pagination_limit(limit: Optional[int], upper_limit: int) -> None: + if limit and upper_limit < limit: + raise NeDRexError(f"limit={limit:,} is too great (maximum is {upper_limit:,})") + + +def download_file(url: str, target: str) -> None: + if config.api_key is not None: + opener = urllib.request.build_opener() + opener.addheaders = [("x-api-key", config.api_key)] + urllib.request.install_opener(opener) + + try: + urllib.request.urlretrieve(url, target) + except urllib.error.HTTPError as err: + if err.code == 404: + raise NeDRexError("not found") from err + raise NeDRexError("unexpected failure") from err + + +def check_status_factory(url_suffix: str) -> Callable[[str], Dict[str, Any]]: + def return_func(uid: str) -> Dict[str, Any]: + url = f"{config.url_base}{url_suffix}" + params = {"uid": uid} + resp = http.get(url, params=params, headers={"x-api-key": config.api_key}) + result: Dict[str, Any] = check_response(resp) + return result + + return return_func diff --git a/python_nedrex/python_nedrex/core.py b/python_nedrex/python_nedrex/core.py new file mode 100644 index 0000000..7e8f38c --- /dev/null +++ b/python_nedrex/python_nedrex/core.py @@ -0,0 +1,229 @@ +"""Module containing functions relating to the general routes in the NeDRex API + +Additionally, this module also contains routes for obtaining API keys. +""" + +from typing import Any as _Any +from typing import Dict as _Dict +from typing import Generator as _Generator +from typing import List as _List +from typing import Optional as _Optional +from typing import cast as _cast + +from python_nedrex import config as _config +from python_nedrex.common import check_pagination_limit as _check_pagination_limit +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import get_pagination_limit as _get_pagination_limit +from python_nedrex.common import http as _http +from python_nedrex.decorators import check_url_base as _check_url_base +from python_nedrex.exceptions import NeDRexError as _NeDRexError + + +def _check_type(coll_name: str, coll_type: str) -> bool: + if coll_type == "edge": + if coll_name in get_edge_types(): + return True + raise _NeDRexError(f"type={coll_name!r} not in NeDRex edge types") + + if coll_type == "node": + if coll_name in get_node_types(): + return True + raise _NeDRexError(f"type={coll_name!r} not in NeDRex node types") + + raise _NeDRexError(f"_check_type received invalid coll_type={coll_type!r}") + + +@_check_url_base +def api_keys_active() -> bool: + """Checks whether API keys are active for the instance of NeDRex set in the config + + Returns True if the keys are active, False otherwise + """ + url = f"{_config.url_base}/api_key_setting" + response = _http.get(url) + if response.status_code != 200: + raise Exception("Unexpected non-200 status code") + return _cast(bool, response.json()) + + +@_check_url_base +def get_api_key(*, accept_eula: bool = False) -> _Any: + """Obtains a new API key from the NeDRex API. + + This function will only return if accept_eula is explicitly set to True + """ + if accept_eula is not True: + raise _NeDRexError("an API key cannot be obtained unless accept_eula is set to True") + + url = f"{_config.url_base}/admin/api_key/generate" + response = _http.post(url, json={"accept_eula": accept_eula}) + return response.json() + + +@_check_url_base +def get_node_types() -> _Any: + """ + Returns the list of node types stored in NeDRexDB + + Returns: + node_list (list[str]): List of node types in NeDRex + """ + url: str = f"{_config.url_base}/list_node_collections" + response = _http.get(url, headers={"x-api-key": _config.api_key}) + node_list = _check_response(response) + return node_list + + +@_check_url_base +def get_edge_types() -> _Any: + """ + Returns a list of edge types stored in NeDRexDB + + Returns: + edge_list (list[str]): List of edge types in NeDRex + """ + url: str = f"{_config.url_base}/list_edge_collections" + response = _http.get(url, headers={"x-api-key": _config.api_key}) + edge_list = _check_response(response) + return edge_list + + +@_check_url_base +def get_collection_attributes(coll_type: str, include_counts: bool = False) -> _Any: + """ + Retrurns a list of the available attributes stored in NeDRex for the given type + + Parameters: + type (str): The node or edge type to get available attributes for + + Returns: + attr_list (list[str]): The list of available attributes for the specified node/edge type + """ + url: str = f"{_config.url_base}/{coll_type}/attributes" + response = _http.get(url, headers={"x-api-key": _config.api_key}, params={"include_counts": include_counts}) + attr_list = _check_response(response) + return attr_list + + +@_check_url_base +def get_node_ids(coll_type: str) -> _Any: + """ + Returns a list of node identifiers in NeDRex for the given type + + Parameters: + type(str): The node type to get IDs for + Returns: + node_ids (list[str]): The list of available node_ids for the specified node type + """ + _check_type(coll_type, "node") + + url: str = f"{_config.url_base}/{coll_type}/attributes/primaryDomainId/json" + + resp = _http.get(url, headers={"x-api-key": _config.api_key}) + data = _check_response(resp) + node_ids = [i["primaryDomainId"] for i in data] + return node_ids + + +@_check_url_base +def get_nodes( + node_type: str, + attributes: _Optional[_List[str]] = None, + node_ids: _Optional[_List[str]] = None, + limit: _Optional[int] = None, + offset: int = 0, +) -> _Any: + """ + Returns nodes in NeDRex for the given type + + Parameters: + node_type (str): The node type to collect + attributes (Optional[list[str]]): A list of attributes to return for the collected nodes. The default + (None) returns all attributes. + node_ids (Optional[list[str]]): A list of the specific node IDs to return. The default (None) returns all + nodes. + limit (Optional[int]): A limit for the number of records to be returned. The maximum value for this is set + by the API. + offset (int): The number of records to skip before returning records. Default is 0 (no records skipped). + Returns: + node_ids (list[str]): The list of available node_ids for the specified node type + """ + _check_type(node_type, "node") + + upper_limit = _get_pagination_limit() + _check_pagination_limit(limit, upper_limit) + + params = {"node_id": node_ids, "attribute": attributes, "offset": offset, "limit": limit} + + resp = _http.get( + f"{_config.url_base}/{node_type}/attributes/json", params=params, headers={"x-api-key": _config.api_key} + ) + + items = _check_response(resp) + return items + + +@_check_url_base +def iter_nodes( + node_type: str, attributes: _Optional[_List[str]] = None, node_ids: _Optional[_List[str]] = None +) -> _Generator[_Dict[str, _Any], None, None]: + + _check_type(node_type, "node") + upper_limit = _get_pagination_limit() + + params: _Dict[str, _Any] = {"node_id": node_ids, "attribute": attributes, "limit": upper_limit} + + offset = 0 + while True: + params["offset"] = offset + resp = _http.get( + f"{_config.url_base}/{node_type}/attributes/json", params=params, headers={"x-api-key": _config.api_key} + ) + + data = _check_response(resp) + + for doc in data: + yield doc + + if len(data) < upper_limit: + break + offset += upper_limit + + +@_check_url_base +def get_edges(edge_type: str, limit: _Optional[int] = None, offset: _Optional[int] = None) -> _Any: + """ + Returns edges in NeDRex of the given type + + Parameters: + edge_type (str): The node type to collect + limit (Optional[int]): A limit for the number of records to be returned. The maximum value for this is set + by the API. + offset (int): The number of records to skip before returning records. Default is 0 (no records skipped). + """ + _check_type(edge_type, "edge") + + params = {"limit": limit, "offset": offset, "api_key": _config.api_key} + + resp = _http.get(f"{_config.url_base}/{edge_type}/all", params=params, headers={"x-api-key": _config.api_key}) + items = _check_response(resp) + return items + + +@_check_url_base +def iter_edges(edge_type: str) -> _Generator[_Dict[str, _Any], None, None]: + _check_type(edge_type, "edge") + upper_limit = _get_pagination_limit() + + offset = 0 + while True: + params = {"offset": offset, "limit": upper_limit} + resp = _http.get(f"{_config.url_base}/{edge_type}/all", params=params, headers={"x-api-key": _config.api_key}) + data = _check_response(resp) + + for doc in data: + yield doc + + if len(data) < upper_limit: + break + offset += upper_limit diff --git a/python_nedrex/python_nedrex/decorators.py b/python_nedrex/python_nedrex/decorators.py new file mode 100644 index 0000000..29ffc65 --- /dev/null +++ b/python_nedrex/python_nedrex/decorators.py @@ -0,0 +1,24 @@ +from typing import Any, Callable, TypeVar + +from python_nedrex import config +from python_nedrex.exceptions import ConfigError + +R = TypeVar("R") + + +def check_url_base(func: Callable[..., R]) -> Callable[..., R]: + def wrapped_fx(*args: Any, **kwargs: Any) -> Any: + if hasattr(config, "url_base") and config.url_base is not None: + return func(*args, **kwargs) + raise ConfigError("API URL is not set in the config") + + return wrapped_fx + + +def check_url_vpd(func: Callable[..., R]) -> Callable[..., R]: + def wrapped_fx(*args: Any, **kwargs: Any) -> Any: + if hasattr(config, "url_vpd") and config.url_vpd is not None: + return func(*args, **kwargs) + raise ConfigError("VPD URL is not set in the config") + + return wrapped_fx diff --git a/python_nedrex/python_nedrex/diamond.py b/python_nedrex/python_nedrex/diamond.py new file mode 100644 index 0000000..1b5fc78 --- /dev/null +++ b/python_nedrex/python_nedrex/diamond.py @@ -0,0 +1,41 @@ +from typing import List as _List + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + + +def diamond_submit( + seeds: _List[str], + n: int, # pylint: disable=C0103 + alpha: int = 1, + network: str = "DEFAULT", + edges: str = "all", +) -> str: + if edges not in {"limited", "all"}: + raise ValueError(f"invalid value for argument edges ({edges!r}), should be all|limited") + + url = f"{_config.url_base}/diamond/submit" + body = { + "seeds": seeds, + "n": n, + "alpha": alpha, + "network": network, + "edges": edges, + } + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +check_diamond_status = _check_status_factory("/diamond/status") + + +def download_diamond_results(uid: str) -> str: + url = f"{_config.url_base}/diamond/download" + params = {"uid": uid} + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp, return_type="text") + return result diff --git a/python_nedrex/python_nedrex/disorder.py b/python_nedrex/python_nedrex/disorder.py new file mode 100644 index 0000000..21502be --- /dev/null +++ b/python_nedrex/python_nedrex/disorder.py @@ -0,0 +1,31 @@ +"""Module containing python functions to access the disorder routes in the NeDRex API""" + +from typing import Any as _Any +from typing import Callable as _Callable +from typing import List as _List +from typing import Union as _Union + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import http as _http +from python_nedrex.decorators import check_url_base as _check_url_base + + +def _generate_route(path: str) -> _Callable[[_Union[str, _List[str]]], _Any]: + @_check_url_base + def new_func(codes: _Union[str, _List[str]]) -> _Any: + if isinstance(codes, str): + codes = [codes] + + url = f"{_config.url_base}/disorder/{path}" + resp = _http.get(url, params={"q": codes}, headers={"x-api-key": _config.api_key}) + return _check_response(resp) + + return new_func + + +search_by_icd10 = _generate_route("get_by_icd10") +get_disorder_descendants = _generate_route("descendants") +get_disorder_ancestors = _generate_route("ancestors") +get_disorder_parents = _generate_route("parents") +get_disorder_children = _generate_route("children") diff --git a/python_nedrex/python_nedrex/domino.py b/python_nedrex/python_nedrex/domino.py new file mode 100644 index 0000000..b21a36f --- /dev/null +++ b/python_nedrex/python_nedrex/domino.py @@ -0,0 +1,18 @@ +from typing import List as _List + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + + +def domino_submit(seeds: _List[str], network: str = "DEFAULT") -> str: + url = f"{_config.url_base}/domino/submit" + body = {"seeds": seeds, "network": network} + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +check_domino_status = _check_status_factory("/domino/status") diff --git a/python_nedrex/python_nedrex/exceptions.py b/python_nedrex/python_nedrex/exceptions.py new file mode 100644 index 0000000..bf9c9c6 --- /dev/null +++ b/python_nedrex/python_nedrex/exceptions.py @@ -0,0 +1,6 @@ +class NeDRexError(Exception): + pass + + +class ConfigError(NeDRexError): + pass diff --git a/python_nedrex/python_nedrex/graph.py b/python_nedrex/python_nedrex/graph.py new file mode 100644 index 0000000..fa89adb --- /dev/null +++ b/python_nedrex/python_nedrex/graph.py @@ -0,0 +1,82 @@ +from pathlib import Path as _Path +from typing import Any as _Any +from typing import Dict as _Dict +from typing import List as _List +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import download_file as _download_file +from python_nedrex.common import http as _http + + +# pylint: disable=R0913 +def build_request( + nodes: _Optional[_List[str]] = None, + edges: _Optional[_List[str]] = None, + ppi_evidence: _Optional[_List[str]] = None, + include_ppi_self_loops: bool = False, + taxid: _Optional[_List[int]] = None, + drug_groups: _Optional[_List[str]] = None, + concise: bool = True, + include_omim: bool = True, + disgenet_threshold: float = 0.0, + use_omim_ids: bool = False, + split_drug_types: bool = False, +) -> str: + + if nodes is None: + nodes = ["disorder", "drug", "gene", "protein"] + if edges is None: + edges = [ + "disorder_is_subtype_of_disorder", + "drug_has_indication", + "drug_has_target", + "gene_associated_with_disorder", + "protein_encoded_by_gene", + "protein_interacts_with_protein", + ] + if ppi_evidence is None: + ppi_evidence = ["exp"] + if taxid is None: + taxid = [9606] + if drug_groups is None: + drug_groups = ["approved"] + + body = { + "nodes": nodes, + "edges": edges, + "ppi_evidence": ppi_evidence, + "ppi_self_loops": include_ppi_self_loops, + "taxid": taxid, + "drug_groups": drug_groups, + "concise": concise, + "include_omim": include_omim, + "disgenet_threshold": disgenet_threshold, + "use_omim_ids": use_omim_ids, + "split_drug_types": split_drug_types, + } + + url = f"{_config.url_base}/graph/builder" + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +# pylint: enable=R0913 + + +def check_build_status(uid: str) -> _Dict[str, _Any]: + url = f"{_config.url_base}/graph/details/{uid}" + resp = _http.get(url, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _Any] = _check_response(resp) + return result + + +def download_graph(uid: str, target: _Optional[str] = None) -> None: + if target is None: + target = str(_Path(f"{uid}.graphml").resolve()) + + url = f"{_config.url_base}/graph/download/{uid}/{uid}.graphml" + + _download_file(url, target) diff --git a/python_nedrex/python_nedrex/kpm.py b/python_nedrex/python_nedrex/kpm.py new file mode 100644 index 0000000..118f53d --- /dev/null +++ b/python_nedrex/python_nedrex/kpm.py @@ -0,0 +1,27 @@ +from typing import Any as _Any +from typing import Dict as _Dict +from typing import List as _List + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import http as _http + + +def kpm_submit(seeds: _List[str], k: int, network: str = "DEFAULT") -> str: + + url = f"{_config.url_base}/kpm/submit" + body = {"seeds": seeds, "k": k, "network": network} + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +def check_kpm_status(uid: str) -> _Dict[str, _Any]: + + url = f"{_config.url_base}/kpm/status" + params = {"uid": uid} + + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _Any] = _check_response(resp) + return result diff --git a/python_nedrex/python_nedrex/must.py b/python_nedrex/python_nedrex/must.py new file mode 100644 index 0000000..8bfd1af --- /dev/null +++ b/python_nedrex/python_nedrex/must.py @@ -0,0 +1,36 @@ +from typing import List as _List + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + + +# pylint: disable=R0913 +def must_request( + seeds: _List[str], + hubpenalty: float, + multiple: bool, + trees: int, + maxit: int, + network: str = "DEFAULT", +) -> str: + body = { + "seeds": seeds, + "network": network, + "hubpenalty": hubpenalty, + "multiple": multiple, + "trees": trees, + "maxit": maxit, + } + + url = f"{_config.url_base}/must/submit" + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +# pylint: enable=R0913 + + +check_must_status = _check_status_factory("/must/status") diff --git a/python_nedrex/python_nedrex/ppi.py b/python_nedrex/python_nedrex/ppi.py new file mode 100644 index 0000000..1fe0552 --- /dev/null +++ b/python_nedrex/python_nedrex/ppi.py @@ -0,0 +1,28 @@ +from typing import Any as _Any +from typing import Dict as _Dict +from typing import Iterable as _Iterable +from typing import List as _List +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_pagination_limit as _check_pagination_limit +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import get_pagination_limit as _get_pagination_limit +from python_nedrex.common import http as _http +from python_nedrex.exceptions import NeDRexError + + +def ppis(evidence: _Iterable[str], skip: int = 0, limit: _Optional[int] = None) -> _List[_Dict[str, _Any]]: + evidence_set = set(evidence) + extra_evidence = evidence_set - {"exp", "pred", "ortho"} + if extra_evidence: + raise NeDRexError(f"unexpected evidence types: {extra_evidence}") + + maximum_limit = _get_pagination_limit() + _check_pagination_limit(limit, maximum_limit) + + params = {"iid_evidence": list(evidence_set), "skip": skip, "limit": limit} + + resp = _http.get(f"{_config.url_base}/ppi", params=params, headers={"x-api-key": _config.api_key}) + result: _List[_Dict[str, _Any]] = _check_response(resp) + return result diff --git a/python_nedrex/python_nedrex/relations.py b/python_nedrex/python_nedrex/relations.py new file mode 100644 index 0000000..d5b7e5c --- /dev/null +++ b/python_nedrex/python_nedrex/relations.py @@ -0,0 +1,87 @@ +from typing import Dict as _Dict +from typing import Iterable as _Iterable +from typing import List as _List +from typing import Union as _Union + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import http as _http + + +def get_encoded_proteins(gene_list: _Iterable[_Union[int, str]]) -> _Dict[str, _List[str]]: + """ + Genes the proteins encoded by genes in a supplied gene list. + + The genes can be submitted either as a list of strings or integers. + """ + genes = [] + for gene in gene_list: + if isinstance(gene, int): + gene = str(gene) + if not isinstance(gene, str): + raise ValueError("items in gene_list must be int or str") + + gene = gene.lower() + if not gene.startswith("entrez."): + genes.append(f"entrez.{gene}") + else: + genes.append(gene) + + url = f"{_config.url_base}/relations/get_encoded_proteins" + resp = _http.get(url, params={"gene": genes}, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _List[str]] = _check_response(resp) + return result + + +def get_drugs_indicated_for_disorders(disorder_list: _Iterable[str]) -> _Dict[str, _List[str]]: + disorders = [] + for disorder in disorder_list: + if not isinstance(disorder, str): + raise ValueError("items in disorder_list must be str") + + if disorder.startswith("mondo."): + disorders.append(disorder) + else: + disorders.append(f"mondo.{disorder}") + + url = f"{_config.url_base}/relations/get_drugs_indicated_for_disorders" + resp = _http.get(url, params={"disorder": disorders}, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _List[str]] = _check_response(resp) + return result + + +def get_drugs_targetting_proteins(protein_list: _Iterable[str]) -> _Dict[str, _List[str]]: + proteins = [] + for protein in protein_list: + if not isinstance(protein, str): + raise ValueError("items in protein_list must be str") + + if protein.startswith("uniprot."): + proteins.append(protein) + else: + proteins.append(f"uniprot.{protein}") + + url = f"{_config.url_base}/relations/get_drugs_targetting_proteins" + resp = _http.get(url, params={"protein": proteins}, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _List[str]] = _check_response(resp) + return result + + +def get_drugs_targetting_gene_products(gene_list: _Iterable[str]) -> _Dict[str, _List[str]]: + genes = [] + for gene in gene_list: + if isinstance(gene, int): + gene = str(gene) + if not isinstance(gene, str): + raise ValueError("items in gene_list must be int or str") + + gene = gene.lower() + if not gene.startswith("entrez."): + genes.append(f"entrez.{gene}") + else: + genes.append(gene) + + url = f"{_config.url_base}/relations/get_drugs_targetting_gene_products" + resp = _http.get(url, params={"gene": genes}, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _List[str]] = _check_response(resp) + return result diff --git a/python_nedrex/python_nedrex/robust.py b/python_nedrex/python_nedrex/robust.py new file mode 100644 index 0000000..57c5455 --- /dev/null +++ b/python_nedrex/python_nedrex/robust.py @@ -0,0 +1,45 @@ +from typing import List as _List + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + + +# pylint: disable=R0913 +def robust_submit( + seeds: _List[str], + network: str = "DEFAULT", + initial_fraction: float = 0.25, + reduction_factor: float = 0.9, + num_trees: int = 30, + threshold: float = 0.1, +) -> str: + + body = { + "seeds": seeds, + "network": network, + "initial_fraction": initial_fraction, + "reduction_factor": reduction_factor, + "num_trees": num_trees, + "threshold": threshold, + } + url = f"{_config.url_base}/robust/submit" + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +# pylint: enable=R0913 + +check_robust_status = _check_status_factory("/robust/status") + + +def download_robust_results(uid: str) -> str: + url = f"{_config.url_base}/robust/results" + params = {"uid": uid} + + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp, return_type="text") + return result diff --git a/python_nedrex/python_nedrex/static.py b/python_nedrex/python_nedrex/static.py new file mode 100644 index 0000000..3f492f8 --- /dev/null +++ b/python_nedrex/python_nedrex/static.py @@ -0,0 +1,31 @@ +from typing import Any as _Any +from typing import Dict as _Dict +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import download_file as _download_file +from python_nedrex.common import http as _http + + +def get_metadata() -> _Dict[str, _Any]: + url = f"{_config.url_base}/static/metadata" + resp = _http.get(url, headers={"x-api-key": _config.api_key}) + result: _Dict[str, _Any] = _check_response(resp) + return result + + +def get_license() -> str: + url = f"{_config.url_base}/static/license" + resp = _http.get(url) + result: str = _check_response(resp) + return result + + +def download_lengths_map(target: _Optional[str] = None) -> None: + if target is None: + target = "lengths.map" + + url = f"{_config.url_base}/static/lengths.map" + + _download_file(url, target) diff --git a/python_nedrex/python_nedrex/trustrank.py b/python_nedrex/python_nedrex/trustrank.py new file mode 100644 index 0000000..36812c4 --- /dev/null +++ b/python_nedrex/python_nedrex/trustrank.py @@ -0,0 +1,41 @@ +from typing import List as _List +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + + +def trustrank_submit( + seeds: _List[str], + damping_factor: float = 0.85, + only_direct_drugs: bool = True, + only_approved_drugs: bool = True, + n: _Optional[int] = None, # pylint: disable=C0103 +) -> str: + url = f"{_config.url_base}/trustrank/submit" + + body = { + "seeds": seeds, + "damping_factor": damping_factor, + "only_direct_drugs": only_direct_drugs, + "only_approved_drugs": only_approved_drugs, + "N": n, + } + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +check_trustrank_status = _check_status_factory("/trustrank/status") + + +def download_trustrank_results(uid: str) -> str: + url = f"{_config.url_base}/trustrank/download" + params = {"uid": uid} + + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp, return_type="text") + return result diff --git a/python_nedrex/python_nedrex/validation.py b/python_nedrex/python_nedrex/validation.py new file mode 100644 index 0000000..599793b --- /dev/null +++ b/python_nedrex/python_nedrex/validation.py @@ -0,0 +1,80 @@ +from typing import List as _List + +from python_nedrex import config as _config +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import check_status_factory as _check_status_factory +from python_nedrex.common import http as _http + +check_validation_status = _check_status_factory("/validation/status") + + +def check_module_member_type(mmt: str) -> None: + if mmt not in {"gene", "protein"}: + raise ValueError(f"module_member_type {mmt!r} is invalid (should be 'gene' or 'protein'") + + +# pylint: disable=R0913 +def joint_validation_submit( + module_members: _List[str], + module_member_type: str, + test_drugs: _List[str], + true_drugs: _List[str], + permutations: int, + only_approved_drugs: bool = True, +) -> str: + check_module_member_type(module_member_type) + + url = f"{_config.url_base}/validation/joint" + body = { + "module_members": module_members, + "module_member_type": module_member_type, + "test_drugs": test_drugs, + "true_drugs": true_drugs, + "permutations": permutations, + "only_approved_drugs": only_approved_drugs, + } + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +# pylint: enable=R0913 + + +def module_validation_submit( + module_members: _List[str], + module_member_type: str, + true_drugs: _List[str], + permutations: int, + only_approved_drugs: bool = True, +) -> str: + check_module_member_type(module_member_type) + + url = f"{_config.url_base}/validation/module" + body = { + "module_members": module_members, + "module_member_type": module_member_type, + "true_drugs": true_drugs, + "permutations": permutations, + "only_approved_drugs": only_approved_drugs, + } + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result + + +def drug_validation_submit( + test_drugs: _List[str], true_drugs: _List[str], permutations: int, only_approved_drugs: bool = True +) -> str: + + url = f"{_config.url_base}/validation/drug" + body = { + "test_drugs": test_drugs, + "true_drugs": true_drugs, + "permutations": permutations, + "only_approved_drugs": only_approved_drugs, + } + + resp = _http.post(url, json=body, headers={"x-api-key": _config.api_key}) + result: str = _check_response(resp) + return result diff --git a/python_nedrex/python_nedrex/variants.py b/python_nedrex/python_nedrex/variants.py new file mode 100644 index 0000000..d6e47a6 --- /dev/null +++ b/python_nedrex/python_nedrex/variants.py @@ -0,0 +1,160 @@ +from typing import Any as _Any +from typing import Dict as _Dict +from typing import Generator as _Generator +from typing import List as _List +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import check_pagination_limit as _check_pagination_limit +from python_nedrex.common import check_response as _check_response +from python_nedrex.common import get_pagination_limit as _get_pagination_limit +from python_nedrex.common import http as _http + + +def get_effect_choices() -> _List[str]: + url = f"{_config.url_base}/variants/get_effect_choices" + resp = _http.get(url, headers={"x-api-key": _config.api_key}) + result: _List[str] = _check_response(resp) + return result + + +def get_review_status_choices() -> _List[str]: + url = f"{_config.url_base}/variants/get_review_choices" + resp = _http.get(url, headers={"x-api-key": _config.api_key}) + result: _List[str] = _check_response(resp) + return result + + +# pylint: disable=R0913 +def get_variant_disorder_associations( + variant_ids: _Optional[_List[str]] = None, + disorder_ids: _Optional[_List[str]] = None, + review_status: _Optional[_List[str]] = None, + effect: _Optional[_List[str]] = None, + limit: _Optional[int] = None, + offset: int = 0, +) -> _List[_Dict[str, _Any]]: + max_limit = _get_pagination_limit() + if isinstance(limit, int): + _check_pagination_limit(limit, max_limit) + else: + limit = max_limit + + params = { + "variant_id": variant_ids, + "disorder_id": disorder_ids, + "review_status": review_status, + "effect": effect, + "limit": limit, + "offset": offset, + } + + url = f"{_config.url_base}/variants/get_variant_disorder_associations" + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: _List[_Dict[str, _Any]] = _check_response(resp) + return result + + +# pylint: enable=R0913 + + +def iter_variant_disorder_associations( + variant_ids: _Optional[_List[str]] = None, + disorder_ids: _Optional[_List[str]] = None, + review_status: _Optional[_List[str]] = None, + effect: _Optional[_List[str]] = None, +) -> _Generator[_Dict[str, _Any], None, None]: + max_limit = _get_pagination_limit() + offset = 0 + + kwargs = { + "variant_ids": variant_ids, + "disorder_ids": disorder_ids, + "review_status": review_status, + "effect": effect, + "limit": max_limit, + "offset": offset, + } + + while True: + results = get_variant_disorder_associations(**kwargs) + if len(results) == 0: + return + yield from results + + offset += max_limit + kwargs["offset"] = offset + + +def get_variant_gene_associations( + variant_ids: _Optional[_List[str]] = None, + gene_ids: _Optional[_List[str]] = None, + limit: _Optional[int] = None, + offset: int = 0, +) -> _List[_Dict[str, _Any]]: + + max_limit = _get_pagination_limit() + if isinstance(limit, int): + _check_pagination_limit(limit, max_limit) + else: + limit = max_limit + + params = { + "variant_id": variant_ids, + "gene_id": gene_ids, + "offset": offset, + "limit": limit, + } + + url = f"{_config.url_base}/variants/get_variant_gene_associations" + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: _List[_Dict[str, _Any]] = _check_response(resp) + return result + + +def iter_variant_gene_associations( + variant_ids: _Optional[_List[str]] = None, + gene_ids: _Optional[_List[str]] = None, +) -> _Generator[_Dict[str, _Any], None, None]: + max_limit = _get_pagination_limit() + offset = 0 + + kwargs = { + "variant_ids": variant_ids, + "gene_ids": gene_ids, + "limit": max_limit, + "offset": offset, + } + + while True: + results = get_variant_gene_associations(**kwargs) + if len(results) == 0: + return + yield from results + + offset += max_limit + kwargs["offset"] = offset + + +def get_variant_based_disorder_associated_genes( + disorder_id: str, review_status: _Optional[_List[str]] = None, effect: _Optional[_List[str]] = None +) -> _List[_Dict[str, _Any]]: + params = {"disorder_id": disorder_id, "review_status": review_status, "effect": effect} + + url = f"{_config.url_base}/variants/variant_based_disorder_associated_genes" + + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: _List[_Dict[str, _Any]] = _check_response(resp) + return result + + +def get_variant_based_gene_associated_disorders( + gene_id: str, review_status: _Optional[_List[str]] = None, effect: _Optional[_List[str]] = None +) -> _List[_Dict[str, _Any]]: + params = {"gene_id": gene_id, "review_status": review_status, "effect": effect} + + url = f"{_config.url_base}/variants/variant_based_gene_associated_disorders" + + resp = _http.get(url, params=params, headers={"x-api-key": _config.api_key}) + result: _List[_Dict[str, _Any]] = _check_response(resp) + return result diff --git a/python_nedrex/python_nedrex/vpd.py b/python_nedrex/python_nedrex/vpd.py new file mode 100644 index 0000000..11689d8 --- /dev/null +++ b/python_nedrex/python_nedrex/vpd.py @@ -0,0 +1,32 @@ +import os as _os +from typing import Optional as _Optional + +from python_nedrex import config as _config +from python_nedrex.common import http as _http +from python_nedrex.decorators import check_url_vpd as _check_url_vpd + + +@_check_url_vpd +def get_vpd(disorder: str, number_of_patients: int, out_dir: str) -> _Optional[str]: + """ + Downloads a .zip archive with the requested virtual patient data to the given directory. + + Parameters: + disorder (str): The disorder mondoID (e.g mondo.0000090) for which the virtual patient should be retrieved. + number_of_patients (int): The number of simulated patients in the dataset. Can be 1, 10 or 100. + out_dir (str): The absolute path of a directory where the virtual patient data should be stored. + + Returns: + archive (str): Absolute path of the downloaded zip archive or None if the requested resource does not exist. + """ + archive_name: str = f"{disorder}_1000GP_{number_of_patients}VPSim.zip" + url: str = f"{_config.url_vpd}/vpd/{disorder}/{archive_name}" + archive: str = _os.path.join(out_dir, archive_name) + + data = _http.get(url) + if data.status_code != 200: + return None + + with open(archive, "wb") as arch: + arch.write(data.content) + return archive diff --git a/python_nedrex/requirements.txt b/python_nedrex/requirements.txt new file mode 100644 index 0000000..2c15053 --- /dev/null +++ b/python_nedrex/requirements.txt @@ -0,0 +1,5 @@ +attrs==21.4.0 +cachetools==4.2.4 +more-itertools==8.13.0 +pytest==7.0.1 +requests==2.27.1 diff --git a/python_nedrex/requirements_dev.txt b/python_nedrex/requirements_dev.txt new file mode 100644 index 0000000..ebb3f1d --- /dev/null +++ b/python_nedrex/requirements_dev.txt @@ -0,0 +1,12 @@ +pip==19.2.3 +bump2version==0.5.11 +wheel==0.33.6 +watchdog==0.9.0 +flake8==3.7.8 +tox==3.14.0 +coverage==4.5.4 +Sphinx==1.8.5 +twine==1.14.0 + +pytest==6.2.4 +black==21.7b0 diff --git a/python_nedrex/setup.cfg b/python_nedrex/setup.cfg new file mode 100644 index 0000000..2490955 --- /dev/null +++ b/python_nedrex/setup.cfg @@ -0,0 +1,20 @@ +[bumpversion] +current_version = 0.1.1 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:python_nedrex/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bdist_wheel] +universal = 1 + +[flake8] +exclude = docs +[tool:pytest] + diff --git a/python_nedrex/setup.py b/python_nedrex/setup.py new file mode 100644 index 0000000..81d1190 --- /dev/null +++ b/python_nedrex/setup.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +"""The setup script.""" + +from setuptools import setup, find_packages + +with open("README.rst") as readme_file: + readme = readme_file.read() + +with open("HISTORY.rst") as history_file: + history = history_file.read() + +requirements = [ + "attrs", + "requests", + "cachetools", +] + +test_requirements = [ + "pytest>=3", +] + +setup( + author="David James Skelton", + author_email="james.skelton@newcastle.ac.uk", + python_requires=">=3.6", + classifiers=[ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ], + description="A Python library for interfacing with the PNeDRex API", + install_requires=requirements, + license="MIT license", + long_description=readme + "\n\n" + history, + include_package_data=True, + keywords="python_nedrex", + name="python_nedrex", + packages=find_packages(include=["python_nedrex", "python_nedrex.*"]), + test_suite="tests", + tests_require=test_requirements, + url="https://github.com/james-skelton/python_nedrex", + version="0.1.1", + zip_safe=False, +) diff --git a/python_nedrex/tests/__init__.py b/python_nedrex/tests/__init__.py new file mode 100644 index 0000000..6aa3522 --- /dev/null +++ b/python_nedrex/tests/__init__.py @@ -0,0 +1 @@ +"""Unit test package for python_nedrex.""" diff --git a/python_nedrex/tests/test_python_nedrex.py b/python_nedrex/tests/test_python_nedrex.py new file mode 100644 index 0000000..262a41d --- /dev/null +++ b/python_nedrex/tests/test_python_nedrex.py @@ -0,0 +1,664 @@ +#!/usr/bin/env python + +"""Tests for `python_nedrex` package.""" + +import os +import re +from pathlib import Path +import random +import tempfile +import time +from contextlib import contextmanager +from functools import lru_cache + +from more_itertools import take + +import pytest +import requests + +import python_nedrex +from python_nedrex.common import get_pagination_limit +from python_nedrex.core import ( + get_edges, + iter_edges, + iter_nodes, + get_node_types, + get_edge_types, + get_collection_attributes, + get_node_ids, + get_nodes, + api_keys_active, +) +from python_nedrex.diamond import diamond_submit, check_diamond_status, download_diamond_results +from python_nedrex.disorder import ( + get_disorder_ancestors, + get_disorder_children, + get_disorder_descendants, + get_disorder_parents, + search_by_icd10, +) +from python_nedrex.domino import ( + domino_submit, + check_domino_status +) +from python_nedrex.exceptions import ConfigError, NeDRexError +from python_nedrex.graph import ( + build_request, + check_build_status, + download_graph, +) +from python_nedrex.must import must_request, check_must_status +from python_nedrex.ppi import ppis +from python_nedrex.relations import ( + get_encoded_proteins, + get_drugs_indicated_for_disorders, + get_drugs_targetting_proteins, + get_drugs_targetting_gene_products, +) + + +API_URL = "http://82.148.225.92:8123/" +API_KEY = requests.post(f"{API_URL}admin/api_key/generate", json={"accept_eula": True}).json() + + +SEEDS = [ + "P43121", + "P01589", + "P30203", + "P21554", + "P01579", + "O43557", + "Q99572", + "P01920", + "P25942", + "P01189", + "P21580", + "Q02556", + "P01584", + "P01574", + "P02649", + "P29466", + "P22301", + "P16581", + "P06276", + "P11473", + "O60333", + "P19256", + "Q96P20", + "P01911", + "Q2KHT3", + "P18510", + "P05362", + "P01903", + "P29597", + "P13232", + "Q13191", + "Q06330", + "P04440", + "P78508", + "P19320", + "P19438", + "P02774", + "O75508", + "P29459", + "P16871", + "Q14765", + "Q16552", +] + +UID_REGEX = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") + + +@contextmanager +def url_base(): + python_nedrex.config.set_url_base(API_URL) + yield + python_nedrex.config._url_base = None + + +@contextmanager +def api_key(): + python_nedrex.config.set_api_key(API_KEY) + yield + python_nedrex.config._api_key = None + + +@lru_cache(maxsize=10) +def get_node_collections(): + with api_key(), url_base(): + collections = get_node_types() + return collections + + +@lru_cache(maxsize=10) +def get_edge_collections(): + with api_key(), url_base(): + collections = get_edge_types() + return collections + + +def get_random_disorder_selection(n, skip_root=True): + random.seed(20220621) + with api_key(), url_base(): + disorder_ids = set(get_node_ids("disorder")) + disorder_ids.remove("mondo.0000001") + return random.sample(sorted(disorder_ids), n) + + +@pytest.fixture +def config(): + return {"api_url": API_URL, "api_key": API_KEY} + + +@pytest.fixture +def set_api_key(config): + with api_key(): + yield + + +@pytest.fixture +def set_base_url(config): + with url_base(): + yield + + +def test_set_api_base(set_base_url): + assert python_nedrex.config._url_base == API_URL.rstrip("/") + + +class TestGetNodeTypes: + @pytest.fixture + def result(self, set_base_url, set_api_key): + result = get_node_types() + return result + + def test_return_type(self, result): + assert isinstance(result, list) + assert all(isinstance(item, str) for item in result) + + def test_ordering(self, result): + assert result == sorted(result) + + def test_content(self, result): + assert "protein" in result + + +class TestGetEdgeTypes: + @pytest.fixture + def result(self, set_base_url, set_api_key): + result = get_edge_types() + return result + + def test_return_type(self, result): + assert isinstance(result, list) + assert all(isinstance(item, str) for item in result) + + def test_ordering(self, result): + assert result == sorted(result) + + def test_content(self, result): + assert "protein_encoded_by_gene" in result + + +class TestGetCollectionAttributes: + @pytest.mark.parametrize("collection", get_node_collections()) + def test_get_node_collection_attributes(self, set_base_url, set_api_key, collection): + expected_attributes = ("primaryDomainId", "domainIds", "type") + coll_attributes = get_collection_attributes(collection) + assert all(attr in coll_attributes for attr in expected_attributes) + + @pytest.mark.parametrize("collection", get_edge_collections()) + def test_get_edge_collection_attributes(self, set_base_url, set_api_key, collection): + # NOTE: Exclude the protein_interacts_with_protein collection because of its size. + coll_attributes = get_collection_attributes(collection) + assert "type" in coll_attributes + + assert all(attr in coll_attributes for attr in ("memberOne", "memberTwo")) or all( + attr in coll_attributes for attr in ("sourceDomainId", "targetDomainId") + ) + + +class TestGetNodeIds: + @pytest.mark.parametrize("collection", get_node_collections()) + def test_get_node_ids(self, set_base_url, set_api_key, collection): + assert get_node_ids(collection) + + @pytest.mark.parametrize("collection", get_edge_collections()) + def test_get_node_ids_fails_for_edges(self, set_base_url, set_api_key, collection): + with pytest.raises(NeDRexError): + get_node_ids(collection) + + +class TestGetEdgeRoutes: + @pytest.mark.parametrize("collection", get_edge_collections()) + def test_return_type_get_edges(self, set_base_url, set_api_key, collection): + edges = get_edges(collection, limit=1_000) + assert isinstance(edges, list) + + @pytest.mark.parametrize("collection", get_edge_collections()) + def test_edge_attributes(self, set_base_url, set_api_key, collection): + result = get_collection_attributes(collection, include_counts=True) + total = result["document_count"] + attr_counts = result["attribute_counts"] + + assert attr_counts["type"] == total + assert ( + attr_counts.get("memberOne", 0) == attr_counts.get("memberTwo", 0) == total + and attr_counts.get("sourceDomainId", 0) == attr_counts.get("targetDomainId", 0) == 0 + ) ^ ( + attr_counts.get("memberOne", 0) == attr_counts.get("memberTwo", 0) == 0 + and attr_counts.get("sourceDomainId", 0) == attr_counts.get("targetDomainId", 0) == total + ) + + +class TestGetNodeRoutes: + @pytest.mark.parametrize("collection", get_node_collections()) + def test_get_all_nodes(self, set_base_url, set_api_key, collection): + assert isinstance(get_nodes(collection), list) + + def test_get_specific_nodes(self, set_base_url, set_api_key): + nodes = get_nodes("disorder", node_ids=["mondo.0000001"]) + assert isinstance(nodes, list) + assert len(nodes) == 1 + assert nodes[0]["primaryDomainId"] == "mondo.0000001" + + def test_get_drugs_with_api_key(self, set_base_url, set_api_key): + nodes = get_nodes("drug") + assert isinstance(nodes, list) + + def test_get_specific_attributes(self, set_base_url, set_api_key): + nodes = get_nodes("disorder", attributes=["displayName"]) + assert isinstance(nodes, list) + assert [set(i.keys()) == {"primaryDomainId", "displayName"} for i in nodes] + + def test_get_specific_attribute_and_nodes(self, set_base_url, set_api_key): + nodes = get_nodes("disorder", attributes=["displayName"], node_ids=["mondo.0000001"]) + assert isinstance(nodes, list) + assert len(nodes) == 1 + assert nodes[0] == { + "displayName": "disease or disorder", + "primaryDomainId": "mondo.0000001", + } + + def test_pagination(self, set_base_url, set_api_key): + nodes = get_nodes("genomic_variant", limit=1000, offset=1000) + assert isinstance(nodes, list) + assert len(nodes) == 1000 + + def test_consistent_pagination(self, set_base_url, set_api_key): + offset = 1234 + limit = 69 + + nodes = get_nodes("genomic_variant", limit=limit, offset=offset) + + for _ in range(10): + nodes_repeat = get_nodes("genomic_variant", limit=limit, offset=offset) + assert nodes_repeat == nodes + + +class TestDisorderRoutes: + def test_search_by_icd10(self, set_base_url, set_api_key): + # NOTE: There is currently an ICD-10 mapping issue due to MONDO + search_by_icd10("I52") + + def test_get_disorder_ancestors(self, set_base_url, set_api_key): + # Check that `disease or disorder`is an ancestor of `lupus nephritis` + # `disease or disorder` is not a parent of `lupus neprhitis` + lupus_nephritis = "mondo.0005556" + disease_or_disorder = "mondo.0000001" + + result = get_disorder_ancestors(lupus_nephritis) + assert disease_or_disorder in result[lupus_nephritis] + + def test_get_disorder_descendants(self, set_base_url, set_api_key): + # Check that `lupus nephritis` is a descendant of `inflammatory disease` + # `lupus nephritis` is not a child of `inflammatory disease` + inflam_disease = "mondo.0021166" + lupus_nephritis = "mondo.0005556" + + result = get_disorder_descendants(inflam_disease) + assert lupus_nephritis in result[inflam_disease] + + def test_get_disorder_parents(self, set_base_url, set_api_key): + # Check that `glomerulonephritis` is a parent of `lupus nephritis` + lupus_nephritis = "mondo.0005556" + glomerulonephritis = "mondo.0002462" + + result = get_disorder_parents("mondo.0005556") + assert glomerulonephritis in result[lupus_nephritis] + + def test_get_disorder_children(self, set_base_url, set_api_key): + # Check that `lupus nephritis` is a child of `glomerulonephritis` + glomerulonephritis = "mondo.0002462" + lupus_nephritis = "mondo.0005556" + + result = get_disorder_children(glomerulonephritis) + assert lupus_nephritis in result[glomerulonephritis] + + @pytest.mark.parametrize("chosen_id", get_random_disorder_selection(20)) + def test_parent_child_reciprocity(self, set_base_url, set_api_key, chosen_id): + parents = get_disorder_parents(chosen_id) + children_of_parents = get_disorder_children(parents[chosen_id]) + assert all(chosen_id in value for value in children_of_parents.values()) + + @pytest.mark.parametrize("chosen_id", get_random_disorder_selection(20)) + def test_ancestor_descendant_reciprocity(self, set_base_url, set_api_key, chosen_id): + parents = get_disorder_ancestors(chosen_id) + descendants_of_parents = get_disorder_descendants(parents[chosen_id]) + assert all(chosen_id in value for value in descendants_of_parents.values()) + + +class TestRoutesFailWithoutAPIUrl: + def test_get_node_type(self, set_api_key): + with pytest.raises(ConfigError) as excinfo: + get_node_types() + assert "API URL is not set in the config" == str(excinfo.value) + + def test_get_edge_type(self, set_api_key): + with pytest.raises(ConfigError) as excinfo: + get_edge_types() + assert "API URL is not set in the config" == str(excinfo.value) + + +class TestRoutesFailWithoutAPIKey: + def test_get_node_type(self, set_base_url): + if not api_keys_active(): + return + + with pytest.raises(ConfigError) as excinfo: + get_node_types() + assert "no API key set in the configuration" == str(excinfo.value) + + if not api_keys_active(): + return + + with pytest.raises(ConfigError) as excinfo: + get_edge_types() + assert "no API key set in the configuration" == str(excinfo.value) + + @pytest.mark.parametrize("collection", get_node_collections()) + def test_node_routes_fail(self, set_base_url, collection): + if not api_keys_active(): + return + + with pytest.raises(ConfigError) as excinfo: + get_collection_attributes(collection) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + get_node_ids(collection) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + get_nodes(collection) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + for _ in take(1, iter_nodes(collection)): + pass + assert "no API key set in the configuration" == str(excinfo.value) + + @pytest.mark.parametrize("collection", get_edge_collections()) + def test_edge_routes_fail(self, set_base_url, collection): + if not api_keys_active(): + return + + with pytest.raises(ConfigError) as excinfo: + get_collection_attributes(collection) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + get_edges(collection) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + for _ in take(1, iter_edges(collection)): + pass + assert "no API key set in the configuration" == str(excinfo.value) + + def test_disorder_routes_fail(self, set_base_url): + disorder_id = "mondo.0000001" # root of the MONDO tree + icd10_id = "I59.1" # Heart disease, unspecified + + with pytest.raises(ConfigError) as excinfo: + get_disorder_children(disorder_id) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + get_disorder_parents(disorder_id) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + get_disorder_ancestors(disorder_id) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + get_disorder_descendants(disorder_id) + assert "no API key set in the configuration" == str(excinfo.value) + + with pytest.raises(ConfigError) as excinfo: + search_by_icd10(icd10_id) + assert "no API key set in the configuration" == str(excinfo.value) + + +class TestPPIRoute: + def test_ppi_route(self, set_base_url, set_api_key): + ppis(["exp"], 0, get_pagination_limit()) + + def test_overlap_with_pagination(self, set_base_url, set_api_key): + page_limit = 1_000 + delta = page_limit // 2 + skip = delta + + previous = ppis(["exp"], 0, page_limit) + + for _ in range(100): + current = ppis(["exp"], skip, page_limit) + assert previous[-delta:] == current[:delta] + previous = current + skip += delta + + def test_each_evidence_type_works(self, set_base_url, set_api_key): + for evidence_type in ["exp", "pred", "ortho"]: + results = ppis([evidence_type], 0, get_pagination_limit()) + assert all(evidence_type in doc["evidenceTypes"] for doc in results) + + def test_fails_with_invalid_type(self, set_base_url, set_api_key): + for evidence_type in ["exps", "pr3d", "orth"]: + with pytest.raises(NeDRexError) as excinfo: + ppis([evidence_type]) + err_val = {evidence_type} + assert str(excinfo.value) == f"unexpected evidence types: {err_val}" + + def test_fails_with_large_limit(self, set_base_url, set_api_key): + page_limit = get_pagination_limit() + with pytest.raises(NeDRexError) as excinfo: + ppis(["exp"], limit=page_limit + 1) + + assert str(excinfo.value) == f"limit={page_limit + 1:,} is too great (maximum is {page_limit:,})" + + +class TestRelationshipRoutes: + def test_get_encoded_proteins(self, set_base_url, set_api_key): + # NOTE: If result changes, check these examples are still accurate. + + histamine_receptor_genes = ["3269", 3274, "entrez.11255"] # HRH1, as str # HRH2, as int # HRH3, as prefix + + results = get_encoded_proteins(histamine_receptor_genes) + + assert "P35367" in results["3269"] + assert "P25021" in results["3274"] + assert "Q9Y5N1" in results["11255"] + + def test_get_drugs_indicated_for_disorders(self, set_base_url, set_api_key): + # NOTE: If result changes, check these examples are still accurate. + + disorders = [ + "mondo.0005393", # Gout + "0005362", # ED + ] + + results = get_drugs_indicated_for_disorders(disorders) + + assert "DB00437" in results["0005393"] # Allopurinol for gout + assert "DB00203" in results["0005362"] # Sildenafil for ED + + def test_get_drugs_targetting_proteins(self, set_base_url, set_api_key): + # NOTE: If result changes, check these examples are still accurate. + + proteins = [ + "P35367", # Histamine H1 receptor, targetted by antihistamines + "uniprot.P03372", # Estrogen receptor α, targetted by ethinylestradiol + ] + + results = get_drugs_targetting_proteins(proteins) + + assert "DB00341" in results["P35367"] + assert "DB00977" in results["P03372"] + + def test_get_drugs_targetting_gene_products(self, set_base_url, set_api_key): + genes = [ + "entrez.3269", # HRH1 gene (product targetted by antihistamines) + 2099, # Estrogen receptor α gene (product targetted by ethinylestradiol) + "6532", # SLC6A4, encodes Sodium-dependent serotonin transporter, targetted by SSRIs + ] + + results = get_drugs_targetting_gene_products(genes) + + assert "DB00341" in results["3269"] + assert "DB00977" in results["2099"] + assert "DB00215" in results["6532"] + + +class TestGraphRoutes: + def test_default_build(self, set_base_url, set_api_key): + build_request() + + @pytest.mark.parametrize( + "kwargs", + [ + {"nodes": ["this_is_not_a_node"]}, + {"edges": ["this_is_not_an_edge"]}, + {"ppi_evidence": ["made_up"]}, + {"taxid": ["human"]}, + ], + ) + def test_build_fails_with_invalid_params(self, kwargs, set_base_url, set_api_key): + with pytest.raises(NeDRexError): + build_request(**kwargs) + + def test_get_uid(self, set_base_url, set_api_key): + uid = build_request() + assert UID_REGEX.match(uid) + check_build_status(uid) + + def test_fails_with_invalid_uid(self, set_base_url, set_api_key): + uid = "this-is-not-a-valid-uid!" + with pytest.raises(NeDRexError): + check_build_status(uid) + + def test_download_graph(self, set_base_url, set_api_key): + uid = build_request() + while True: + status = check_build_status(uid) + if status["status"] == "completed": + break + time.sleep(10) + + download_graph(uid) + p = Path(f"{uid}.graphml") + assert p.exists() + p.unlink() + + def test_download_graph_different_dir(self, set_base_url, set_api_key): + with tempfile.TemporaryDirectory() as tmpdir: + + uid = build_request() + while True: + status = check_build_status(uid) + if status["status"] == "completed": + break + time.sleep(10) + + target = os.path.join(tmpdir, "mygraph.graphml") + + download_graph(uid, target) + p = Path(target) + assert p.exists() + p.unlink() + + def test_download_fails_with_invalid_uid(self, set_base_url, set_api_key): + uid = "this-is-not-a-valid-uid!" + with pytest.raises(NeDRexError): + download_graph(uid) + + +class TestMustRoutes: + def test_simple_request(self, set_base_url, set_api_key): + uid = must_request(SEEDS, 0.5, True, 10, 2) + assert UID_REGEX.match(uid) + + def test_must_status(self, set_base_url, set_api_key): + uid = must_request(SEEDS, 0.5, True, 10, 2) + status = check_must_status(uid) + assert isinstance(status, dict) + assert "status" in status.keys() + + @pytest.mark.parametrize( + "update", + [ + {"hubpenalty": 1.01}, + {"hubpenalty": -0.01}, + {"hubpenalty": None}, + {"multiple": None}, + {"trees": -1}, + {"trees": None}, + {"maxit": -1}, + {"maxit": None}, + ], + ) + def test_must_fails_with_invalid_arguments(self, set_base_url, set_api_key, update): + kwargs = {"seeds": SEEDS, "hubpenalty": 0.5, "multiple": True, "trees": 10, "maxit": 2, "network": "DEFAULT"} + + with pytest.raises(NeDRexError): + kwargs = {**kwargs, **update} + must_request(**kwargs) + + +class TestDiamondRoutes: + def test_simple_request(self, set_base_url, set_api_key): + uid = diamond_submit(SEEDS, 10) + assert UID_REGEX.match(uid) + + def test_diamond_status(self, set_base_url, set_api_key): + uid = diamond_submit(SEEDS, 10) + status = check_diamond_status(uid) + assert isinstance(status, dict) + assert "status" in status.keys() + + def test_diamond_download(self, set_base_url, set_api_key): + uid = diamond_submit(SEEDS, 10) + + while True: + status = check_diamond_status(uid) + if status["status"] == "completed": + break + time.sleep(10) + + download_diamond_results(uid) + + def test_diamond_fails_with_invalid_arguments(self, set_base_url, set_api_key): + with pytest.raises(ValueError): + diamond_submit(SEEDS, n=10, edges="some") + + +class TestDominoRoutes: + def test_simple_request(self, set_base_url, set_api_key): + uid = domino_submit(SEEDS) + assert UID_REGEX.match(uid) + + def test_check_domino_status(self, set_base_url, set_api_key): + uid = domino_submit(SEEDS) + status = check_domino_status(uid) + assert isinstance(status, dict) + assert "status" in status.keys() \ No newline at end of file diff --git a/python_nedrex/tox.ini b/python_nedrex/tox.ini new file mode 100644 index 0000000..57d5be8 --- /dev/null +++ b/python_nedrex/tox.ini @@ -0,0 +1,26 @@ +[tox] +envlist = py36, py37, py38, flake8 + +[travis] +python = + 3.8: py38 + 3.7: py37 + 3.6: py36 + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 python_nedrex tests + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +deps = + -r{toxinidir}/requirements_dev.txt +; If you want to make tox run the tests with the same versions, create a +; requirements.txt with the pinned versions and uncomment the following line: +; -r{toxinidir}/requirements.txt +commands = + pip install -U pip + pytest --basetemp={envtmpdir} + -- GitLab