From 9bb96a74756cc35193f66b702d595569ccd8d60e Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 16 Oct 2024 17:30:13 +0900 Subject: [PATCH] initial working version --- .github/ISSUE_TEMPLATE/bug_report.md | 43 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++ .github/workflows/build_and_release.yml | 149 ++++++++++++++++ .gitignore | 183 ++++++++++++++++++++ README.md | 93 ++++++++++ build_python.sh | 107 ++++++++++++ docs/CHANGELOG.md | 5 + python/LICENSE | 202 ++++++++++++++++++++++ python/MANIFEST.in | 3 + python/pyproject.toml | 148 ++++++++++++++++ python/src/apbs_binary/__init__.py | 51 ++++++ python/src/apbs_binary/executable.py | 134 ++++++++++++++ requirements_test.txt | 2 + tests/__init__.py | 0 tests/test_wheel.py | 123 +++++++++++++ 15 files changed, 1262 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/build_and_release.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100755 build_python.sh create mode 100644 docs/CHANGELOG.md create mode 100644 python/LICENSE create mode 100644 python/MANIFEST.in create mode 100644 python/pyproject.toml create mode 100644 python/src/apbs_binary/__init__.py create mode 100644 python/src/apbs_binary/executable.py create mode 100644 requirements_test.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_wheel.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..c14f5d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: 버그 수정 +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' +--- + +**버그에 대해 설명해 주세요 :)** + +[..] 할 때 [..] 에러가 나며 트레이닝이 멈춥니다. +(GIF, 동영상 등 드래그해서 첨부할 수도 있습니다.) + +**To Reproduce** + +버그 만드는 법: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** + +원래대로라면 [...] 해야 합니다. + +**로그 파일을 붙여넣어 주세요 :)** + +
+ + +여기에 붙여넣어 주세요 + + +
+ +**사용하고 계시는 프로그램의 버전을 입력해 주세요 :)** + +``` +여기에 적어주세요 +``` + +**기타 내용을 적어주세요 :)** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6d868f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: 기능 추가 요청 +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**겪고 계신 문제를 설명해 주세요 :)** +[...] 할때 항상 [...] 문제가 있습니다. + +**원하시는 솔루션을 설명해 주세요 :)** + +**다른 대안을 고려해 보셨다면 설명해 주세요 :)** + +**사용하고 계시는 프로그램의 버전을 적어주세요 :)** + +**기타 내용을 적어주세요 :)** diff --git a/.github/workflows/build_and_release.yml b/.github/workflows/build_and_release.yml new file mode 100644 index 0000000..cf627c2 --- /dev/null +++ b/.github/workflows/build_and_release.yml @@ -0,0 +1,149 @@ +name: Build and release to PyPI +on: + workflow_dispatch: + inputs: + apbs-release-tag: + description: APBS version to build + required: true + default: 3.4.1 + version-tag: + description: Python package version to release to PyPI (without 'v') + required: true + default: 3.4.1.post1 + dry-run: + description: Dry run + type: boolean + default: false + exclude-types: + description: Commit types to exclude from the changelog + required: false + default: build,docs,style,other + +jobs: + build-all-platforms: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + pip install uv --break-system-packages + uv tool install build + uv tool install wheel + uv tool install huggingface_hub + huggingface-cli download --repo-type dataset --local-dir data Deargen/py-apbs-binary + - name: Build python wheels + run: | + bash build_python.sh ${{ inputs.apbs-release-tag }} ${{ inputs.version-tag }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels + path: dist/*.whl + + test-ubuntu: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] + # python-version: ['3.8'] + # os: [ubuntu-20.04] + needs: [build-all-platforms] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: dist + name: wheels + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Test wheel + run: | + pip install uv --break-system-packages + uv venv + source .venv/bin/activate + uv pip install -r requirements_test.txt + uv pip install dist/*-manylinux*_x86_64.whl + pytest + + test-macos: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + os: [macos-12, macos-13, macos-14, macos-15] + # os: [macos-14, macos-15] + needs: [build-all-platforms] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: dist + name: wheels + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Test wheel + run: | + pip install uv --break-system-packages + uv venv --python python3 + source .venv/bin/activate + uv pip install -r requirements_test.txt + if [[ ${{ matrix.os }} == 'macos-12' ]] || [[ ${{ matrix.os }} == 'macos-13' ]]; then + uv pip install dist/*-macosx_*_x86_64.whl + else + uv pip install dist/*-macosx_*_arm64.whl + fi + pytest + + # test-windows: + # runs-on: ${{ matrix.os }} + # strategy: + # matrix: + # python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + # os: [windows-2019, windows-2022] + # needs: [build-all-platforms] + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/download-artifact@v4 + # with: + # path: dist + # name: wheels + # - uses: actions/setup-python@v5 + # with: + # python-version: ${{ matrix.python-version }} + # - name: Test wheel + # shell: pwsh + # run: | + # pip install uv --break-system-packages + # uv venv + # .venv\Scripts\activate + # uv pip install -r requirements_test.txt + # uv pip install (get-item dist/*-win_amd64.whl) + # pytest + + commit-changelog-and-release-github: + needs: [test-ubuntu, test-macos] + uses: deargen/workflows/.github/workflows/commit-changelog-and-release.yml@master + with: + version-tag: ${{ github.event.inputs.version-tag }} + dry-run: ${{ github.event.inputs.dry-run == 'true' }} + changelog-path: docs/CHANGELOG.md + exclude-types: ${{ github.event.inputs.exclude-types }} + + release: + name: Release + if: ${{ github.event.inputs.dry-run == 'false' }} + runs-on: ubuntu-24.04 + needs: [commit-changelog-and-release-github] + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + name: wheels + - name: Build and upload to PyPI + run: | + pip install uv --break-system-packages + uv tool install twine + twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --non-interactive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f958bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +/data/ + +# some files created when calling apbs? +io.mc +.DS_Store + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python + diff --git a/README.md b/README.md new file mode 100644 index 0000000..84d193a --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# pip install apbs-binary (Unofficial binary distribution of APBS) + +[![image](https://img.shields.io/pypi/v/apbs-binary.svg)](https://pypi.python.org/pypi/apbs-binary) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/apbs-binary)](https://pypistats.org/packages/apbs-binary) +[![image](https://img.shields.io/pypi/l/apbs-binary.svg)](https://pypi.python.org/pypi/apbs-binary) +[![image](https://img.shields.io/pypi/pyversions/apbs-binary.svg)](https://pypi.python.org/pypi/apbs-binary) + + +Install and use [APBS](https://github.com/Electrostatics/apbs) with ease in Python. + +```bash +pip install apbs-binary +``` + +```python +from apbs_binary import APBS_BIN_PATH, MULTIVALUE_BIN_PATH, apbs, multivalue, process_run + +# Run apbs +print(APBS_BIN_PATH) +apbs("--help") + +# Run multivalue +print(MULTIVALUE_BIN_PATH) +multivalue() +``` + +The other tools are also available, but not all of them have the function wrappers. Use as `apbs_binary.process_run("analysis", "--help")` for example. + +Supported platforms: + +- Linux x86_64 (Ubuntu 20.04 +) +- MacOS x86_64 (11.6+), arm64 (12.0+) + +> [!NOTE] +> Installing the package does NOT put the binary in $PATH. +> Instead, the API will tell you where it is located. + +## 👨‍💻️ Maintenance Notes + +### Releasing a new version with CI (recommended) + +Go to Github Actions and run the `Build and Release` workflow. + +Version rule: + +3.4.1.post2: 3.4.1 is the APBS version, postN can increase with the changes of the package and the builds. + + +### Running locally + +This section describes how it works. + +To run it locally, first install the dependencies: + +```bash +pip install uv --user --break-system-packages +uv tool install wheel +uv tool install build +uv tool install huggingface_hub + +# Mac +brew install gnu-sed +``` + +Download the built binaries into `data/`: + +```bash +huggingface-cli download --repo-type dataset --local-dir data Deargen/py-apbs-binary +``` + +Build four wheels on all platforms. Basically it will put the binaries in the `src/apbs_binary/bin/` folder and then build the wheels in a temp directory, outputting the wheels to `dist/`. + +```bash +# first arg: APBS version to find in `data/` (i.e. data/apbs-3.4.1/APBS-3.4.1.Linux) +# second arg: wheel version +bash build_python.sh 3.4.1 3.4.1.post2 +``` + +Test the wheel + +```bash +uv venv +source .venv/bin/activate +uv pip install -r requirements_test.txt +uv pip install build_python/dist/*.whl # choose the wheel for your platform only. +pytest +``` + +### Notes + +- The official 3.4.1 binary is not static and depends on python39.dll, libpython3.9.so.1.0, etc. on Windows and Mac (Linux seems fine) +- `brew install brewsci/bio/apbs` is not static either and depends on libmetis.dylib. +- In this repository, we use the brew-created binary for macOS and the official binary for Linux. We don't support Windows yet. diff --git a/build_python.sh b/build_python.sh new file mode 100755 index 0000000..02c3952 --- /dev/null +++ b/build_python.sh @@ -0,0 +1,107 @@ +if [ "$#" -ne 2 ]; then + echo "Build python wheels in a temp directory." + echo "Usage: $0 " + echo "Example: $0 3.4.1 3.4.1.post4" + exit 1 +fi + +APBS_VERSION="$1" +PY_PACKAGE_VERSION="$2" + +# use gsed on mac +if [[ "$OSTYPE" == "darwin"* ]]; then + SED="gsed" +else + SED="sed" +fi + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +mkdir -p "$SCRIPT_DIR/dist" + +prepare_python_in_temp_dir() { + TEMP_DIR=$(mktemp -d) + PYTHON_BUILD_DIR="$TEMP_DIR/python" + PYTHON_PACKAGE_DIR="$PYTHON_BUILD_DIR/src/apbs_binary" + echo "Temp directory: $TEMP_DIR" + echo "Python build directory: $PYTHON_BUILD_DIR" + + # copy the python project to the temp directory + cp -r "$SCRIPT_DIR/python" "$TEMP_DIR" + cp "$SCRIPT_DIR/README.md" "$PYTHON_BUILD_DIR" + mkdir "$PYTHON_BUILD_DIR/src/apbs_binary/bin" + + # Replace version = "0.0.0" with the desired version + $SED -i "s/version = \"0.0.0\"/version = \"$PY_PACKAGE_VERSION\"/g" "$PYTHON_BUILD_DIR/pyproject.toml" || { echo "Failure"; exit 1; } + # Replace __version__ = "0.0.0" with the desired version + $SED -i "s/__version__ = \"0.0.0\"/__version__ = \"$PY_PACKAGE_VERSION\"/g" "$PYTHON_PACKAGE_DIR/__init__.py" || { echo "Failure"; exit 1; } +} + +build_wheel() { + cd "$PYTHON_BUILD_DIR" || { echo "Failure"; exit 1; } + pyproject-build --installer=uv --wheel + wheel tags --python-tag py3 --abi-tag none --platform "$PLATFORM_NAME" dist/*.whl --remove + mv "$PYTHON_BUILD_DIR/dist/"*.whl "$SCRIPT_DIR/dist" +} + +# extract each binary in the temp directory +# 1. macosx_12_0_arm64 +prepare_python_in_temp_dir + +PLATFORM_NAME="macosx_12_0_arm64" +tar xvzf "$SCRIPT_DIR/data/apbs-$APBS_VERSION/macosx_12_0_arm64/apbs.tar.gz" -C "$TEMP_DIR" +tar xvzf "$SCRIPT_DIR/data/apbs-$APBS_VERSION/macosx_12_0_arm64/metis.tar.gz" -C "$TEMP_DIR" +mv "$TEMP_DIR/apbs/${APBS_VERSION}_2/bin/apbs" "$PYTHON_PACKAGE_DIR/bin" +mv "$TEMP_DIR/apbs/${APBS_VERSION}_2/share/apbs/tools/bin/"* "$PYTHON_PACKAGE_DIR/bin" +mkdir -p "$PYTHON_PACKAGE_DIR/lib" +mv "$TEMP_DIR/metis/5.1.0/lib/libmetis.dylib" "$PYTHON_PACKAGE_DIR/lib" + +build_wheel + +# 2. macosx_10_15_x86_64 +prepare_python_in_temp_dir + +PLATFORM_NAME="macosx_10_15_x86_64" +# unzip "$SCRIPT_DIR/data/apbs-${APBS_VERSION}/APBS-${APBS_VERSION}.Darwin.zip" -d "$TEMP_DIR" +# mv "$TEMP_DIR/APBS-${APBS_VERSION}.Darwin/bin/apbs" "$PYTHON_PACKAGE_DIR/bin" +# mv "$TEMP_DIR/APBS-${APBS_VERSION}.Darwin/share/apbs/tools/bin/"* "$PYTHON_PACKAGE_DIR/bin" +tar xvzf "$SCRIPT_DIR/data/apbs-$APBS_VERSION/macosx_10_15_x86_64/apbs.tar.gz" -C "$TEMP_DIR" +tar xvzf "$SCRIPT_DIR/data/apbs-$APBS_VERSION/macosx_10_15_x86_64/metis.tar.gz" -C "$TEMP_DIR" +mv "$TEMP_DIR/apbs/${APBS_VERSION}_2/bin/apbs" "$PYTHON_PACKAGE_DIR/bin" +mv "$TEMP_DIR/apbs/${APBS_VERSION}_2/share/apbs/tools/bin/"* "$PYTHON_PACKAGE_DIR/bin" +mkdir -p "$PYTHON_PACKAGE_DIR/lib" +mv "$TEMP_DIR/metis/5.1.0/lib/libmetis.dylib" "$PYTHON_PACKAGE_DIR/lib" + +build_wheel + +# 3. Linux +prepare_python_in_temp_dir + +# NOTE: the actual platform is manylinux_2_30_x86_64, but it won't be compatible with uv pip compile. +# Thus we use manylinux_2_28_x86_64, for now. +# It will crash when running in Ubuntu 18.04 but still succeed to install. +PLATFORM_NAME="manylinux_2_28_x86_64" +# rm -rf "${PYTHON_PACKAGE_DIR:?}/bin" # ${var:?} to ensure it doesn't expand to /bin ! +# mkdir "$PYTHON_PACKAGE_DIR/bin" +unzip "$SCRIPT_DIR/data/apbs-${APBS_VERSION}/APBS-${APBS_VERSION}.Linux.zip" -d "$TEMP_DIR" +mv "$TEMP_DIR/APBS-${APBS_VERSION}.Linux/bin/apbs" "$PYTHON_PACKAGE_DIR/bin" +mv "$TEMP_DIR/APBS-${APBS_VERSION}.Linux/share/apbs/tools/bin/"* "$PYTHON_PACKAGE_DIR/bin" + +build_wheel + +# 4. Windows +# NOTE: failed to execute the binary because of lack of python39.dll +# and adding it to PATH didn't work. +# Thus, we disable windows, for now. +# +# prepare_python_in_temp_dir +# +# PLATFORM_NAME="win_amd64" +# # rm -rf "${PYTHON_PACKAGE_DIR:?}/bin" # ${var:?} to ensure it doesn't expand to /bin ! +# # mkdir "$PYTHON_PACKAGE_DIR/bin" +# unzip "$SCRIPT_DIR/data/apbs-${APBS_VERSION}/APBS-${APBS_VERSION}.Windows.zip" -d "$TEMP_DIR" +# mv "$TEMP_DIR/APBS-${APBS_VERSION}.Windows/bin/apbs.exe" "$PYTHON_PACKAGE_DIR/bin" +# mv "$TEMP_DIR/APBS-${APBS_VERSION}.Windows/bin/*.dll" "$PYTHON_PACKAGE_DIR/bin" +# mv "$TEMP_DIR/APBS-${APBS_VERSION}.Windows/share/apbs/tools/bin/Release/"*.exe "$PYTHON_PACKAGE_DIR/bin" +# cp "$SCRIPT_DIR/python39.dll" "$PYTHON_PACKAGE_DIR/bin" + +build_wheel diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..d6637e0 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/python/LICENSE b/python/LICENSE new file mode 100644 index 0000000..32eff92 --- /dev/null +++ b/python/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Deargen Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 0000000..c2f0b2d --- /dev/null +++ b/python/MANIFEST.in @@ -0,0 +1,3 @@ +include src/**/* +include src/**/bin/* +include src/**/lib/* diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..fb6d697 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,148 @@ +[build-system] +requires = ["setuptools>=60"] +build-backend = "setuptools.build_meta" + +[project] +name = "apbs-binary" # CHANGE +version = "0.0.0" +description = "Install apbs with pip" # OPTIONALLY CHANGE +authors = [ + { name = "Kiyoon Kim" }, # OPTIONALLY CHANGE +] +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8,<4" +classifiers = [ + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: Apache Software License", +] +keywords = ["biology"] + +[project.urls] +"Homepage" = "https://github.com/deargen/py-apbs-binary" # OPTIONALLY CHANGE + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +addopts = "--cov=ml_project" # CHANGE (name of the importing module name) +testpaths = ["tests"] + +[tool.ruff] +src = ["src"] # for ruff isort +namespace-packages = ["tools", "scripts"] # for INP rule, suppress on these directories + +[tool.ruff.lint] +# OPTIONALLY ADD MORE LATER +select = [ + # flake8 + "E", + "F", + "W", + "B", # Bugbear + "D", # Docstring + "D213", # Multi-line docstring summary should start at the second line (replace D212) + "N", # Naming + "C4", # flake8-comprehensions + "UP", # pyupgrade + "SIM", # simplify + "RUF", # ruff-specific + "RET501", # return + "RET502", # return + "RET503", # return + "PTH", # path + "NPY", # numpy + "PD", # pandas + "PYI", # type stubs for pyright/pylance + "PT", # pytest + "PIE", # + "LOG", # logging + "COM818", # comma misplaced + "COM819", # comma + "DTZ", # datetime + "YTT", + "ASYNC", + "FBT", # boolean trap + "A", # Shadowing python builtins + "EXE", # executable (shebang) + "FA", # future annotations + "ISC", # Implicit string concatenation + "ICN", # Import convention + "INP", # Implicit namespace package (no __init__.py) + "Q", # Quotes + "RSE", # raise + "SLOT", # __slots__ + "PL", # Pylint + "TRY", # try + "FAST", # FastAPI + "AIR", # airflow + "DOC", # docstring + + # Not important + "T10", # debug statements + "T20", # print statements +] + +ignore = [ + "E402", # Module level import not at top of file + "W293", # Blank line contains whitespace + "W291", # Trailing whitespace + "D10", # Missing docstring in public module / function / etc. + "D200", # One-line docstring should fit on one line with quotes + "D212", # Multi-line docstring summary should start at the first line + "D417", # require documentation for every function parameter. + "D401", # require an imperative mood for all docstrings. + "DOC201", # missing Return field in docstring + "PTH123", # Path.open should be used instead of built-in open + "PT006", # Pytest parameterize style + "N812", # Lowercase `functional` imported as non-lowercase `F` (import torch.nn.functional as F) + "NPY002", # legacy numpy random + "UP017", # datetime.timezone.utc -> datetime.UTC + "SIM108", # use ternary operator instead of if-else + "TRY003", # long message in except +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.pycodestyle] +# Black or ruff will enforce line length to be 88, except for docstrings and comments. +# We set it to 120 so we have more space for docstrings and comments. +max-line-length = 120 + +[tool.ruff.lint.isort] +# combine-as-imports = true +known-third-party = ["wandb"] + +## Uncomment this if you want to use Python < 3.10 +# required-imports = [ +# "from __future__ import annotations", +# ] + +[tool.ruff.lint.flake8-tidy-imports] +# Ban certain modules from being imported at module level, instead requiring +# that they're imported lazily (e.g., within a function definition, if TYPE_CHECKING, etc.) +# NOTE: Ruff code TID is currently disabled, so this settings doesn't do anything. +banned-module-level-imports = ["torch", "tensorflow"] + +[tool.ruff.lint.pylint] +max-args = 10 +max-bool-expr = 10 +max-statements = 100 + +[tool.pyright] +include = ["src"] + +typeCheckingMode = "standard" +useLibraryCodeForTypes = true +autoImportCompletions = true + +# pythonPlatform = "Linux" diff --git a/python/src/apbs_binary/__init__.py b/python/src/apbs_binary/__init__.py new file mode 100644 index 0000000..4673e1b --- /dev/null +++ b/python/src/apbs_binary/__init__.py @@ -0,0 +1,51 @@ +from .executable import ( + ANALYSIS_BIN_PATH, + APBS_BIN_PATH, + BENCHMARK_BIN_PATH, + BIN_DIR, + BORN_BIN_PATH, + COULOMB_BIN_PATH, + DEL2DX_BIN_PATH, + DX2MOL_BIN_PATH, + DX2UHBD_BIN_PATH, + DXMATH_BIN_PATH, + MERGEDX2_BIN_PATH, + MERGEDX_BIN_PATH, + MGMESH_BIN_PATH, + MULTIVALUE_BIN_PATH, + SIMILARITY_BIN_PATH, + SMOOTH_BIN_PATH, + TENSOR2DX_BIN_PATH, + UHBD_ASC2BIN_BIN_PATH, + VALUE_BIN_PATH, + apbs, + multivalue, + process_run, +) + +__version__ = "0.0.0" + +__all__ = [ + "ANALYSIS_BIN_PATH", + "APBS_BIN_PATH", + "BENCHMARK_BIN_PATH", + "BIN_DIR", + "BORN_BIN_PATH", + "COULOMB_BIN_PATH", + "DEL2DX_BIN_PATH", + "DX2MOL_BIN_PATH", + "DX2UHBD_BIN_PATH", + "DXMATH_BIN_PATH", + "MERGEDX2_BIN_PATH", + "MERGEDX_BIN_PATH", + "MGMESH_BIN_PATH", + "MULTIVALUE_BIN_PATH", + "SIMILARITY_BIN_PATH", + "SMOOTH_BIN_PATH", + "TENSOR2DX_BIN_PATH", + "UHBD_ASC2BIN_BIN_PATH", + "VALUE_BIN_PATH", + "apbs", + "multivalue", + "process_run", +] diff --git a/python/src/apbs_binary/executable.py b/python/src/apbs_binary/executable.py new file mode 100644 index 0000000..44bb862 --- /dev/null +++ b/python/src/apbs_binary/executable.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import os +import platform +import subprocess +import sys +from copy import deepcopy +from pathlib import Path +from typing import Any, Literal, overload + +BIN_DIR = Path(__file__).parent / "bin" +# NOTE: lib/ only exists for macos arm64, because we built using homebrew +# and it didn't build entirely statically. +# Thus we have to pass DYLD_LIBRARY_PATH to the subprocess.run call. +LIB_DIR = Path(__file__).parent / "lib" + + +def bin_path(bin_name: str) -> Path: + if os.name == "nt": + return BIN_DIR / f"{bin_name}.exe" + return BIN_DIR / bin_name + + +# bin/ +APBS_BIN_PATH = bin_path("apbs") + +# originally share/apbs/tools/bin/, but moved to bin/ +ANALYSIS_BIN_PATH = bin_path("analysis") +BENCHMARK_BIN_PATH = bin_path("benchmark") +BORN_BIN_PATH = bin_path("born") +COULOMB_BIN_PATH = bin_path("coulomb") +DEL2DX_BIN_PATH = bin_path("del2dx") +DX2MOL_BIN_PATH = bin_path("dx2mol") +DX2UHBD_BIN_PATH = bin_path("dx2uhbd") +DXMATH_BIN_PATH = bin_path("dxmath") +MERGEDX_BIN_PATH = bin_path("mergedx") +MERGEDX2_BIN_PATH = bin_path("mergedx2") +MGMESH_BIN_PATH = bin_path("mgmesh") +MULTIVALUE_BIN_PATH = bin_path("multivalue") +SIMILARITY_BIN_PATH = bin_path("similarity") +SMOOTH_BIN_PATH = bin_path("smooth") +TENSOR2DX_BIN_PATH = bin_path("tensor2dx") +UHBD_ASC2BIN_BIN_PATH = bin_path("uhbd_asc2bin") +VALUE_BIN_PATH = bin_path("value") + + +@overload +def process_run( + bin_name, *args: str, return_completed_process: Literal[True], **kwargs: Any +) -> subprocess.CompletedProcess[str | bytes]: ... + + +@overload +def process_run( + bin_name, *args: str, return_completed_process: Literal[False] = ..., **kwargs: Any +) -> int: ... + + +@overload +def process_run( + bin_name, *args: str, return_completed_process: bool = False, **kwargs: Any +) -> int | subprocess.CompletedProcess[str | bytes]: ... + + +def process_run( + bin_name, *args: str, return_completed_process: bool = False, **kwargs: Any +) -> int | subprocess.CompletedProcess[str | bytes]: + # if mac arm64, set DYLD_LIBRARY_PATH + if sys.platform == "darwin": + my_env: dict[str, str] + if kwargs.get("env") is not None: + my_env = deepcopy(kwargs["env"]) + my_env["DYLD_LIBRARY_PATH"] = str(LIB_DIR) + kwargs.pop("env") + else: + my_env = os.environ.copy() + my_env["DYLD_LIBRARY_PATH"] = str(LIB_DIR) + complete_process = subprocess.run( + [bin_path(bin_name), *args], env=my_env, **kwargs + ) + # elif os.name == "nt": + # # dll files are together with the binaries. + # # so we need to add the directory to PATH + # my_env = os.environ.copy() + # my_env["PATH"] = f"{BIN_DIR!s};{my_env['PATH']}" + # complete_process = subprocess.run( + # [bin_path(bin_name), *args], env=my_env, shell=True, **kwargs + # ) + else: + complete_process = subprocess.run([bin_path(bin_name), *args], **kwargs) + + if return_completed_process: + return complete_process + return complete_process.returncode + + +@overload +def apbs( + *args: str, return_completed_process: Literal[True], **kwargs: Any +) -> subprocess.CompletedProcess[str | bytes]: ... + + +@overload +def apbs( + *args: str, return_completed_process: Literal[False] = ..., **kwargs: Any +) -> int: ... + + +def apbs( + *args: str, return_completed_process: bool = False, **kwargs: Any +) -> int | subprocess.CompletedProcess[str | bytes]: + return process_run( + "apbs", *args, return_completed_process=return_completed_process, **kwargs + ) + + +@overload +def multivalue( + *args: str, return_completed_process: Literal[True], **kwargs: Any +) -> subprocess.CompletedProcess[str | bytes]: ... + + +@overload +def multivalue( + *args: str, return_completed_process: Literal[False] = ..., **kwargs: Any +) -> int: ... + + +def multivalue( + *args: str, return_completed_process: bool = False, **kwargs: Any +) -> int | subprocess.CompletedProcess[str | bytes]: + return process_run( + "multivalue", *args, return_completed_process=return_completed_process, **kwargs + ) diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..799e328 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,2 @@ +pytest +regex diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_wheel.py b/tests/test_wheel.py new file mode 100644 index 0000000..679da63 --- /dev/null +++ b/tests/test_wheel.py @@ -0,0 +1,123 @@ +import os +from collections.abc import Callable +from pathlib import Path + +import pytest +from apbs_binary import ( + APBS_BIN_PATH, + MULTIVALUE_BIN_PATH, + apbs, + multivalue, + process_run, +) + +RETURN_CODE_NO_ARGS = [ + ("apbs", 13), + ("multivalue", 1), + ("analysis", 2), + ("benchmark", 12), + ("born", 154), + ("coulomb", 154), + ("del2dx", 1), + ("dx2mol", 1), + ("dx2uhbd", 1), + ("dxmath", 13), + ("mergedx", 255), + ("mergedx2", 1), + ("mgmesh", 0), + ("similarity", 2), + ("smooth", 2), + ("tensor2dx", 255), + ("uhbd_asc2bin", -11), + ("value", 2), +] + + +@pytest.mark.parametrize( + "bin_path", + [ + APBS_BIN_PATH, + MULTIVALUE_BIN_PATH, + ], +) +def test_exists(bin_path: Path): + assert bin_path.is_file() + + +@pytest.mark.parametrize( + ("bin_path", "expected_name"), + [ + (APBS_BIN_PATH, "apbs"), + (MULTIVALUE_BIN_PATH, "multivalue"), + ], +) +def test_name(bin_path: Path, expected_name: str): + assert bin_path.name == expected_name or bin_path.name == expected_name + ".exe" + + +def test_execute_help(): + """ + Note: + multivalue does not have a --help option. + """ + return_code = apbs("--help") + assert return_code == 13 + + +@pytest.mark.parametrize( + ("func", "expected_return_code"), + [ + (apbs, 13), + (multivalue, 1), + ], +) +def test_execute_noarg(func: Callable, expected_return_code: int): + return_code = func() + assert return_code == expected_return_code + + +@pytest.mark.parametrize( + ("bin_name", "expected_return_code"), + RETURN_CODE_NO_ARGS, +) +def test_process_run_noarg(bin_name: str, expected_return_code: int): + return_code = process_run(bin_name) + assert return_code == expected_return_code + + +@pytest.mark.parametrize( + ("bin_name", "expected_return_code"), + RETURN_CODE_NO_ARGS, +) +def test_process_run_noarg_with_env(bin_name: str, expected_return_code: int): + """ + In macOS, the env variable `DYLD_LIBRARY_PATH` is required to run the binaries. Thus, we check if custom env settings still respect this. + """ + my_env = os.environ.copy() + my_env["PATH"] = "/usr/local/bin" # dummy change. Not important + return_code = process_run(bin_name, env=my_env) + assert return_code == expected_return_code + + +@pytest.mark.parametrize( + ("func", "line_number", "line_match"), + [ + (apbs, 3, r"APBS -- Adaptive Poisson-Boltzmann Solver"), + ( + multivalue, + 2, + r"Usage: multivalue [outputformat]", + ), + ], +) +def test_execute_noarg_message_stdout( + func: Callable, line_number: int, line_match: str +): + proc = func(return_completed_process=True, capture_output=True, text=True) + print(proc) + if isinstance(proc.stdout, bytes): + lines = proc.stdout.decode("utf-8") + else: + lines = proc.stdout + line = lines.splitlines()[line_number] + assert line.strip() == line_match