pydustmasker 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ name: PyPi Publish
2
+ on:
3
+ release:
4
+ types:
5
+ - published
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ linux:
12
+ runs-on: ${{ matrix.platform.runner }}
13
+ strategy:
14
+ matrix:
15
+ platform:
16
+ - runner: ubuntu-latest
17
+ target: x86_64
18
+ - runner: ubuntu-latest
19
+ target: x86
20
+ - runner: ubuntu-latest
21
+ target: aarch64
22
+ - runner: ubuntu-latest
23
+ target: armv7
24
+ - runner: ubuntu-latest
25
+ target: s390x
26
+ - runner: ubuntu-latest
27
+ target: ppc64le
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: 3.x
33
+ - name: Build wheels
34
+ uses: PyO3/maturin-action@v1
35
+ with:
36
+ target: ${{ matrix.platform.target }}
37
+ args: --release --out dist --find-interpreter
38
+ sccache: 'true'
39
+ manylinux: auto
40
+ - name: Upload wheels
41
+ uses: actions/upload-artifact@v4
42
+ with:
43
+ name: wheels-linux-${{ matrix.platform.target }}
44
+ path: dist
45
+
46
+ musllinux:
47
+ runs-on: ${{ matrix.platform.runner }}
48
+ strategy:
49
+ matrix:
50
+ platform:
51
+ - runner: ubuntu-latest
52
+ target: x86_64
53
+ - runner: ubuntu-latest
54
+ target: x86
55
+ - runner: ubuntu-latest
56
+ target: aarch64
57
+ - runner: ubuntu-latest
58
+ target: armv7
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: actions/setup-python@v5
62
+ with:
63
+ python-version: 3.x
64
+ - name: Build wheels
65
+ uses: PyO3/maturin-action@v1
66
+ with:
67
+ target: ${{ matrix.platform.target }}
68
+ args: --release --out dist --find-interpreter
69
+ sccache: 'true'
70
+ manylinux: musllinux_1_2
71
+ - name: Upload wheels
72
+ uses: actions/upload-artifact@v4
73
+ with:
74
+ name: wheels-musllinux-${{ matrix.platform.target }}
75
+ path: dist
76
+
77
+ windows:
78
+ runs-on: ${{ matrix.platform.runner }}
79
+ strategy:
80
+ matrix:
81
+ platform:
82
+ - runner: windows-latest
83
+ target: x64
84
+ - runner: windows-latest
85
+ target: x86
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - uses: actions/setup-python@v5
89
+ with:
90
+ python-version: 3.x
91
+ architecture: ${{ matrix.platform.target }}
92
+ - name: Build wheels
93
+ uses: PyO3/maturin-action@v1
94
+ with:
95
+ target: ${{ matrix.platform.target }}
96
+ args: --release --out dist --find-interpreter
97
+ sccache: 'true'
98
+ - name: Upload wheels
99
+ uses: actions/upload-artifact@v4
100
+ with:
101
+ name: wheels-windows-${{ matrix.platform.target }}
102
+ path: dist
103
+
104
+ macos:
105
+ runs-on: ${{ matrix.platform.runner }}
106
+ strategy:
107
+ matrix:
108
+ platform:
109
+ - runner: macos-12
110
+ target: x86_64
111
+ - runner: macos-14
112
+ target: aarch64
113
+ steps:
114
+ - uses: actions/checkout@v4
115
+ - uses: actions/setup-python@v5
116
+ with:
117
+ python-version: 3.x
118
+ - name: Build wheels
119
+ uses: PyO3/maturin-action@v1
120
+ with:
121
+ target: ${{ matrix.platform.target }}
122
+ args: --release --out dist --find-interpreter
123
+ sccache: 'true'
124
+ - name: Upload wheels
125
+ uses: actions/upload-artifact@v4
126
+ with:
127
+ name: wheels-macos-${{ matrix.platform.target }}
128
+ path: dist
129
+
130
+ sdist:
131
+ runs-on: ubuntu-latest
132
+ steps:
133
+ - uses: actions/checkout@v4
134
+ - name: Build sdist
135
+ uses: PyO3/maturin-action@v1
136
+ with:
137
+ command: sdist
138
+ args: --out dist
139
+ - name: Upload sdist
140
+ uses: actions/upload-artifact@v4
141
+ with:
142
+ name: wheels-sdist
143
+ path: dist
144
+
145
+ release:
146
+ name: Release
147
+ runs-on: ubuntu-latest
148
+ if: "startsWith(github.ref, 'refs/tags/')"
149
+ needs: [linux, musllinux, windows, macos, sdist]
150
+ steps:
151
+ - uses: actions/download-artifact@v4
152
+ - name: Publish to PyPI
153
+ uses: PyO3/maturin-action@v1
154
+ env:
155
+ MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
156
+ with:
157
+ command: upload
158
+ args: --non-interactive --skip-existing wheels-*/*
@@ -0,0 +1,80 @@
1
+ name: Testing
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ env:
9
+ CARGO_TERM_COLOR: always
10
+ RUST_BACKTRACE: 1
11
+ RUSTFLAGS: "-D warnings"
12
+ PYTHON_VERSION: "3.9"
13
+ jobs:
14
+ clippy:
15
+ name: Clippy
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Install Rust
20
+ run: |
21
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
22
+ - name: Cache dependencies
23
+ uses: Swatinem/rust-cache@v2
24
+ - name: Run cargo clippy
25
+ run: cargo clippy --all-targets -- --deny warnings
26
+ fmt:
27
+ name: Rustfmt
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - name: Install Rust
32
+ run: |
33
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
34
+ - name: Cache dependencies
35
+ uses: Swatinem/rust-cache@v2
36
+ - name: Run cargo fmt
37
+ run: cargo fmt --all -- --check
38
+ python-linting:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - name: Set up Python
43
+ uses: actions/setup-python@v5
44
+ with:
45
+ python-version: ${{ env.PYTHON_VERSION }}
46
+ cache: "pip"
47
+ - name: Install Dependencies
48
+ run: |
49
+ python -m pip install -U pip
50
+ python -m pip install -r requirements-dev.txt
51
+ python -m pip install -e .
52
+ maturin build --out dist
53
+ - name: Ruff format check
54
+ run: ruff format pydustmasker tests --check
55
+ - name: Lint with ruff
56
+ run: ruff check .
57
+ - name: mypy check
58
+ run: mypy .
59
+ testing:
60
+ strategy:
61
+ fail-fast: false
62
+ matrix:
63
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
64
+ os: [ubuntu-latest, windows-latest, macos-latest]
65
+ runs-on: ${{ matrix.os }}
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ - name: Set up Python ${{ matrix.python-version }}
69
+ uses: actions/setup-python@v5
70
+ with:
71
+ python-version: ${{ matrix.python-version }}
72
+ cache: "pip"
73
+ - name: Install Dependencies
74
+ run: |
75
+ python -m pip install -U pip
76
+ python -m pip install -r requirements-dev.txt
77
+ python -m pip install -e .
78
+ maturin build --out dist
79
+ - name: Test with pytest
80
+ run: pytest
@@ -0,0 +1,141 @@
1
+
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # OS Files
8
+ *.swp
9
+ *.DS_Store
10
+
11
+ # C extensions
12
+ *.so
13
+
14
+ # Distribution / packaging
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ pip-wheel-metadata/
29
+ share/python-wheels/
30
+ *.egg-info/
31
+ .installed.cfg
32
+ *.egg
33
+ MANIFEST
34
+
35
+ # PyInstaller
36
+ # Usually these files are written by a python script from a template
37
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
38
+ *.manifest
39
+ *.spec
40
+
41
+ # Installer logs
42
+ pip-log.txt
43
+ pip-delete-this-directory.txt
44
+
45
+ # Unit test / coverage reports
46
+ htmlcov/
47
+ .tox/
48
+ .nox/
49
+ .coverage
50
+ .coverage.*
51
+ .cache
52
+ nosetests.xml
53
+ coverage.xml
54
+ *.cover
55
+ *.py,cover
56
+ .hypothesis/
57
+ .pytest_cache/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+
69
+ # Flask stuff:
70
+ instance/
71
+ .webassets-cache
72
+
73
+ # Scrapy stuff:
74
+ .scrapy
75
+
76
+ # Sphinx documentation
77
+ docs/_build/
78
+
79
+ # PyBuilder
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100
+ __pypackages__/
101
+
102
+ # Celery stuff
103
+ celerybeat-schedule
104
+ celerybeat.pid
105
+
106
+ # SageMath parsed files
107
+ *.sage.py
108
+
109
+ # Environments
110
+ .env
111
+ .venv
112
+ env/
113
+ venv/
114
+ ENV/
115
+ env.bak/
116
+ venv.bak/
117
+
118
+ # Spyder project settings
119
+ .spyderproject
120
+ .spyproject
121
+
122
+ # Rope project settings
123
+ .ropeproject
124
+
125
+ # mkdocs documentation
126
+ /site
127
+
128
+ # mypy
129
+ .mypy_cache/
130
+ .dmypy.json
131
+ dmypy.json
132
+
133
+ # Pyre type checker
134
+ .pyre/
135
+
136
+ # editors
137
+ .idea
138
+ .vscode
139
+
140
+ # Rust
141
+ /target
@@ -0,0 +1,20 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.6.0
4
+ hooks:
5
+ - id: check-added-large-files
6
+ - id: check-toml
7
+ - id: check-yaml
8
+ - id: debug-statements
9
+ - id: end-of-file-fixer
10
+ - id: trailing-whitespace
11
+ - repo: https://github.com/pre-commit/mirrors-mypy
12
+ rev: v1.11.2
13
+ hooks:
14
+ - id: mypy
15
+ - repo: https://github.com/astral-sh/ruff-pre-commit
16
+ rev: v0.6.5
17
+ hooks:
18
+ - id: ruff
19
+ args: [--fix, --exit-non-zero-on-fix]
20
+ - id: ruff-format
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2024-10-02
9
+ ### Added
10
+ - First release.
@@ -0,0 +1,195 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 3
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.4.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
10
+
11
+ [[package]]
12
+ name = "cfg-if"
13
+ version = "1.0.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
16
+
17
+ [[package]]
18
+ name = "heck"
19
+ version = "0.5.0"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
22
+
23
+ [[package]]
24
+ name = "indoc"
25
+ version = "2.0.5"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
28
+
29
+ [[package]]
30
+ name = "libc"
31
+ version = "0.2.159"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
34
+
35
+ [[package]]
36
+ name = "memoffset"
37
+ version = "0.9.1"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
40
+ dependencies = [
41
+ "autocfg",
42
+ ]
43
+
44
+ [[package]]
45
+ name = "once_cell"
46
+ version = "1.20.1"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
49
+ dependencies = [
50
+ "portable-atomic",
51
+ ]
52
+
53
+ [[package]]
54
+ name = "portable-atomic"
55
+ version = "1.9.0"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
58
+
59
+ [[package]]
60
+ name = "proc-macro2"
61
+ version = "1.0.86"
62
+ source = "registry+https://github.com/rust-lang/crates.io-index"
63
+ checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
64
+ dependencies = [
65
+ "unicode-ident",
66
+ ]
67
+
68
+ [[package]]
69
+ name = "pydustmasker"
70
+ version = "0.1.0"
71
+ dependencies = [
72
+ "pyo3",
73
+ "thiserror",
74
+ ]
75
+
76
+ [[package]]
77
+ name = "pyo3"
78
+ version = "0.22.3"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
81
+ dependencies = [
82
+ "cfg-if",
83
+ "indoc",
84
+ "libc",
85
+ "memoffset",
86
+ "once_cell",
87
+ "portable-atomic",
88
+ "pyo3-build-config",
89
+ "pyo3-ffi",
90
+ "pyo3-macros",
91
+ "unindent",
92
+ ]
93
+
94
+ [[package]]
95
+ name = "pyo3-build-config"
96
+ version = "0.22.3"
97
+ source = "registry+https://github.com/rust-lang/crates.io-index"
98
+ checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
99
+ dependencies = [
100
+ "once_cell",
101
+ "target-lexicon",
102
+ ]
103
+
104
+ [[package]]
105
+ name = "pyo3-ffi"
106
+ version = "0.22.3"
107
+ source = "registry+https://github.com/rust-lang/crates.io-index"
108
+ checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
109
+ dependencies = [
110
+ "libc",
111
+ "pyo3-build-config",
112
+ ]
113
+
114
+ [[package]]
115
+ name = "pyo3-macros"
116
+ version = "0.22.3"
117
+ source = "registry+https://github.com/rust-lang/crates.io-index"
118
+ checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
119
+ dependencies = [
120
+ "proc-macro2",
121
+ "pyo3-macros-backend",
122
+ "quote",
123
+ "syn",
124
+ ]
125
+
126
+ [[package]]
127
+ name = "pyo3-macros-backend"
128
+ version = "0.22.3"
129
+ source = "registry+https://github.com/rust-lang/crates.io-index"
130
+ checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
131
+ dependencies = [
132
+ "heck",
133
+ "proc-macro2",
134
+ "pyo3-build-config",
135
+ "quote",
136
+ "syn",
137
+ ]
138
+
139
+ [[package]]
140
+ name = "quote"
141
+ version = "1.0.37"
142
+ source = "registry+https://github.com/rust-lang/crates.io-index"
143
+ checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
144
+ dependencies = [
145
+ "proc-macro2",
146
+ ]
147
+
148
+ [[package]]
149
+ name = "syn"
150
+ version = "2.0.79"
151
+ source = "registry+https://github.com/rust-lang/crates.io-index"
152
+ checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
153
+ dependencies = [
154
+ "proc-macro2",
155
+ "quote",
156
+ "unicode-ident",
157
+ ]
158
+
159
+ [[package]]
160
+ name = "target-lexicon"
161
+ version = "0.12.16"
162
+ source = "registry+https://github.com/rust-lang/crates.io-index"
163
+ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
164
+
165
+ [[package]]
166
+ name = "thiserror"
167
+ version = "1.0.64"
168
+ source = "registry+https://github.com/rust-lang/crates.io-index"
169
+ checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
170
+ dependencies = [
171
+ "thiserror-impl",
172
+ ]
173
+
174
+ [[package]]
175
+ name = "thiserror-impl"
176
+ version = "1.0.64"
177
+ source = "registry+https://github.com/rust-lang/crates.io-index"
178
+ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
179
+ dependencies = [
180
+ "proc-macro2",
181
+ "quote",
182
+ "syn",
183
+ ]
184
+
185
+ [[package]]
186
+ name = "unicode-ident"
187
+ version = "1.0.13"
188
+ source = "registry+https://github.com/rust-lang/crates.io-index"
189
+ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
190
+
191
+ [[package]]
192
+ name = "unindent"
193
+ version = "0.2.3"
194
+ source = "registry+https://github.com/rust-lang/crates.io-index"
195
+ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "pydustmasker"
3
+ version = "0.1.0"
4
+ description = "Python bindings to DustMasker, a utility to identify and mask low-complexity regions in nucleotide sequences"
5
+ edition = "2021"
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [lib]
10
+ name = "_pydustmasker"
11
+ crate-type = ["cdylib"]
12
+
13
+ [dependencies]
14
+ pyo3 = { version = "0.22.3", features = ["extension-module"] }
15
+ thiserror = "1.0.63"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Antonio Camargo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.3
2
+ Name: pydustmasker
3
+ Version: 0.1.0
4
+ License-File: LICENSE
5
+ Summary: Python bindings to DustMasker, a utility to identify and mask low-complexity regions in nucleotide sequences
6
+ Author-email: Antonio Camargo <antoniop.camargo@gmail.com>
7
+ License: MIT
8
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
9
+
10
+ # pydustmasker
11
+
12
+ `pydustmasker` is a Python library that provides an efficient implementation of the SDUST algorithm[^1], designed to identify and mask low-complexity regions in nucleotide sequences.
13
+
14
+ ## Usage
15
+
16
+ `pydustmasker` provides a `DustMasker` class that enables identification of low-complexity regions in an input DNA sequence and mask these regions.
17
+
18
+ Here is a basic example of how to use `pydustmasker`:
19
+
20
+ ```python
21
+ import pydustmasker
22
+
23
+ # Example nucleotide sequence
24
+ masker = pydustmasker.DustMasker("CGTATATATATAGTATGCGTACTGGGGGGGCT")
25
+
26
+ # Get the low-complexity regions in the sequence and the number of masked bases
27
+ >>> print(masker.intervals)
28
+ [(23, 30)]
29
+ >>> print(masker.n_masked_bases)
30
+ 7
31
+
32
+ # The mask() method returns the sequence with low-complexity regions soft-masked
33
+ >>> print(masker.mask())
34
+ CGTATATATATAGTATGCGTACTgggggggCT
35
+
36
+ # Hard-masking can be enabled by setting the `hard` parameter to `True`
37
+ >>> print(masker.mask(hard=True))
38
+ CGTATATATATAGTATGCGTACTNNNNNNNCT
39
+
40
+ # The `window_size` and `score_threshold` parameters can be adjusted to tune the masking
41
+ >>> masker = pydustmasker.DustMasker(
42
+ ... "CGTATATATATAGTATGCGTACTGGGGGGGCT",
43
+ ... score_threshold=10
44
+ ... )
45
+ >>> print(masker.intervals)
46
+ [(2, 12), (23, 30)]
47
+ >>> print(masker.mask())
48
+ CGtatatatataGTATGCGTACTgggggggCT
49
+ ```
50
+
51
+ [^1]: Morgulis, Aleksandr, et al. "[A fast and symmetric DUST implementation to mask low-complexity DNA sequences](https://doi.org/10.1089/cmb.2006.13.1028)". *Journal of Computational Biology* **13.5** (2006): 1028-1040.
@@ -0,0 +1,42 @@
1
+ # pydustmasker
2
+
3
+ `pydustmasker` is a Python library that provides an efficient implementation of the SDUST algorithm[^1], designed to identify and mask low-complexity regions in nucleotide sequences.
4
+
5
+ ## Usage
6
+
7
+ `pydustmasker` provides a `DustMasker` class that enables identification of low-complexity regions in an input DNA sequence and mask these regions.
8
+
9
+ Here is a basic example of how to use `pydustmasker`:
10
+
11
+ ```python
12
+ import pydustmasker
13
+
14
+ # Example nucleotide sequence
15
+ masker = pydustmasker.DustMasker("CGTATATATATAGTATGCGTACTGGGGGGGCT")
16
+
17
+ # Get the low-complexity regions in the sequence and the number of masked bases
18
+ >>> print(masker.intervals)
19
+ [(23, 30)]
20
+ >>> print(masker.n_masked_bases)
21
+ 7
22
+
23
+ # The mask() method returns the sequence with low-complexity regions soft-masked
24
+ >>> print(masker.mask())
25
+ CGTATATATATAGTATGCGTACTgggggggCT
26
+
27
+ # Hard-masking can be enabled by setting the `hard` parameter to `True`
28
+ >>> print(masker.mask(hard=True))
29
+ CGTATATATATAGTATGCGTACTNNNNNNNCT
30
+
31
+ # The `window_size` and `score_threshold` parameters can be adjusted to tune the masking
32
+ >>> masker = pydustmasker.DustMasker(
33
+ ... "CGTATATATATAGTATGCGTACTGGGGGGGCT",
34
+ ... score_threshold=10
35
+ ... )
36
+ >>> print(masker.intervals)
37
+ [(2, 12), (23, 30)]
38
+ >>> print(masker.mask())
39
+ CGtatatatataGTATGCGTACTgggggggCT
40
+ ```
41
+
42
+ [^1]: Morgulis, Aleksandr, et al. "[A fast and symmetric DUST implementation to mask low-complexity DNA sequences](https://doi.org/10.1089/cmb.2006.13.1028)". *Journal of Computational Biology* **13.5** (2006): 1028-1040.
@@ -0,0 +1,7 @@
1
+ from pydustmasker._pydustmasker import DustMasker
2
+ from pydustmasker._version import VERSION
3
+
4
+ __version__ = VERSION
5
+
6
+
7
+ __all__ = ["DustMasker"]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ class DustMasker:
6
+ sequence: str
7
+ window_size: int
8
+ score_threshold: int
9
+ intervals: Sequence[tuple[int, int]]
10
+ def __init__(
11
+ self, sequence: str, window_size: int, score_threshold: int
12
+ ) -> None: ...
13
+ @property
14
+ def n_masked_bases(self) -> int: ...
15
+ def mask(self, hard: bool) -> str: ...
16
+ def __repr__(self) -> str: ...
@@ -0,0 +1 @@
1
+ VERSION = "0.1.0"
File without changes
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.5,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "pydustmasker"
7
+ description = "Python bindings to DustMasker, a utility to identify and mask low-complexity regions in nucleotide sequences"
8
+ authors = [{ name = "Antonio Camargo", email = "antoniop.camargo@gmail.com" }]
9
+ license = "MIT"
10
+ readme = "README.md"
11
+
12
+ [tool.maturin]
13
+ module-name = "pydustmasker._pydustmasker"
14
+ binding = "pyo3"
15
+ features = ["pyo3/extension-module"]
16
+
17
+ [tool.mypy]
18
+ check_untyped_defs = true
19
+ disallow_untyped_defs = true
20
+
21
+ [[tool.mypy.overrides]]
22
+ module = ["tests.*"]
23
+ disallow_untyped_defs = false
24
+
25
+ [tool.pytest.ini_options]
26
+ minversion = "6.0"
27
+ addopts = "--cov=pydustmasker --cov-report term-missing --no-cov-on-fail"
28
+
29
+ [tool.coverage.report]
30
+ exclude_lines = ["if __name__ == .__main__.:", "pragma: no cover"]
31
+
32
+ [tool.ruff]
33
+ line-length = 89
34
+ target-version = "py39"
35
+ fix = true
36
+
37
+ [tool.ruff.lint]
38
+ select = [
39
+ "E", # pycodestyle
40
+ "B", # flake8-bugbear
41
+ "W", # Warning
42
+ "F", # pyflakes
43
+ "UP", # pyupgrade
44
+ "I001", # unsorted-imports
45
+ "T201", # print found
46
+ "T203", # pprint found
47
+ ]
48
+ ignore = [
49
+ # Recommended ignores by ruff when using formatter
50
+ "E501",
51
+ "W191",
52
+ "E111",
53
+ "E114",
54
+ "E117",
55
+ "D206",
56
+ "D300",
57
+ "Q000",
58
+ "Q001",
59
+ "Q002",
60
+ "Q003",
61
+ "COM812",
62
+ "COM819",
63
+ "ISC001",
64
+ "ISC002",
65
+ ]
@@ -0,0 +1,7 @@
1
+ mypy==1.11.2
2
+ pre-commit==3.8.0
3
+ pytest==8.3.3
4
+ pytest-cov==5.0.0
5
+ ruff==0.6.5
6
+ maturin==1.7.1
7
+ -e .
@@ -0,0 +1,142 @@
1
+ mod sdust;
2
+
3
+ use crate::sdust::SymmetricDust;
4
+ use pyo3::{exceptions::PyValueError, prelude::*};
5
+ use thiserror::Error;
6
+
7
+ const MIN_SEQUENCE_LENGTH: usize = 4;
8
+ const MIN_WINDOW_SIZE: usize = 3;
9
+
10
+ #[derive(Error, Debug)]
11
+ pub enum InputError {
12
+ #[error("sequence is too short, it must be at least 4 characters long")]
13
+ SequenceLengthError(usize),
14
+ #[error("invalid window size '{0}', must be at least '3'")]
15
+ WindowSizeError(usize),
16
+ }
17
+
18
+ fn validate_inputs(sequence: &str, window_size: usize) -> Result<(), InputError> {
19
+ if sequence.len() < MIN_SEQUENCE_LENGTH {
20
+ return Err(InputError::SequenceLengthError(sequence.len()));
21
+ }
22
+ if window_size < MIN_WINDOW_SIZE {
23
+ return Err(InputError::WindowSizeError(window_size));
24
+ }
25
+ Ok(())
26
+ }
27
+
28
+ /// Identify and mask low-complexity regions in nucleotide sequences using the
29
+ /// symmetric DUST algorithm from DustMasker.
30
+ ///
31
+ /// Parameters
32
+ /// ----------
33
+ /// sequence : str
34
+ /// A string representing the nucleotide sequence to be processed. Characters
35
+ /// other than 'A', 'C', 'G', 'T', 'a', 'c', 'g', 't' will be considered
36
+ /// ambiguous bases. The minimum allowed sequence length is 4 bases.
37
+ /// window_size : int, default: 64
38
+ /// The length of the window used by symmetric DUST algorithm. The minimum
39
+ /// allowed value is 3.
40
+ /// score_threshold : int, default: 20
41
+ /// Score threshold for subwindows. The minimum allowed value is 0.
42
+ ///
43
+ /// Attributes
44
+ /// ----------
45
+ /// sequence : str
46
+ /// A string representing the nucleotide sequence that was provided as input.
47
+ /// window_size : int
48
+ /// The length of the window used by symmetric DUST algorithm.
49
+ /// score_threshold : int
50
+ /// Score threshold for subwindows.
51
+ /// Intervals: list of tuples
52
+ /// A immutable list of tuples representing the start and end positions of
53
+ /// the low-complexity regions identified in the sequence.
54
+ /// n_masked_bases : int
55
+ /// The total number of bases that were masked.
56
+ ///
57
+ /// Raises
58
+ /// ------
59
+ /// ValueError
60
+ /// If the input sequence is too short (less than 4 characters) or if the
61
+ /// window size is too small (less than 3).
62
+ /// TypeError
63
+ /// If the input parameters are not of the expected type.
64
+ /// OverflowError
65
+ /// If a negative integer is passed as the window size or score threshold.
66
+ #[pyclass]
67
+ struct DustMasker {
68
+ #[pyo3(get)]
69
+ sequence: String,
70
+ #[pyo3(get)]
71
+ window_size: usize,
72
+ #[pyo3(get)]
73
+ score_threshold: usize,
74
+ #[pyo3(get)]
75
+ intervals: Vec<(usize, usize)>,
76
+ }
77
+
78
+ #[pymethods]
79
+ impl DustMasker {
80
+ #[new]
81
+ #[pyo3(signature = (sequence, window_size=64, score_threshold=20))]
82
+ fn new(sequence: String, window_size: usize, score_threshold: usize) -> PyResult<DustMasker> {
83
+ validate_inputs(&sequence, window_size)
84
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
85
+ let intervals = SymmetricDust::process(sequence.as_bytes(), window_size, score_threshold);
86
+ Ok(DustMasker {
87
+ sequence,
88
+ window_size,
89
+ score_threshold,
90
+ intervals,
91
+ })
92
+ }
93
+ #[getter]
94
+ fn n_masked_bases(&self) -> usize {
95
+ self.intervals.iter().map(|(start, end)| end - start).sum()
96
+ }
97
+ /// Returns the sequence with low-complexity regions masked.
98
+ ///
99
+ /// Parameters
100
+ /// ----------
101
+ /// hard : bool, default: False
102
+ /// If True, low-complexity regions will be masked with 'N' characters.
103
+ /// By default, bases within low-complexity regions are converted to
104
+ /// lowercase (i.e., soft-masking).
105
+ ///
106
+ /// Raises
107
+ /// ------
108
+ /// TypeError
109
+ /// If the input parameters are not of the expected type.
110
+ #[pyo3(signature = (hard=false))]
111
+ fn mask(&self, hard: bool) -> PyResult<String> {
112
+ let mut masked_sequence = self.sequence.clone();
113
+ for &(start, end) in &self.intervals {
114
+ if hard {
115
+ let len = end - start;
116
+ masked_sequence.replace_range(start..end, &"N".repeat(len));
117
+ } else {
118
+ let lowercased = self.sequence[start..end].to_lowercase();
119
+ masked_sequence.replace_range(start..end, &lowercased);
120
+ }
121
+ }
122
+ Ok(masked_sequence)
123
+ }
124
+ fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
125
+ let sequence_preview = if slf.borrow().sequence.len() > 8 {
126
+ format!("{}…", &slf.borrow().sequence[..8])
127
+ } else {
128
+ slf.borrow().sequence.clone()
129
+ };
130
+ Ok(format!(
131
+ "DustMasker(sequence: '{}', intervals: {:?})",
132
+ sequence_preview,
133
+ slf.borrow().intervals
134
+ ))
135
+ }
136
+ }
137
+
138
+ #[pymodule]
139
+ fn _pydustmasker(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
140
+ m.add_class::<DustMasker>()?;
141
+ Ok(())
142
+ }
@@ -0,0 +1,253 @@
1
+ // Code adapted from: https://crates.io/crates/sdust
2
+
3
+ use std::collections::VecDeque;
4
+ use std::ops::Range;
5
+
6
+ const MASK: u8 = 63;
7
+ const ENCODING_LOOKUP: [u8; 256] = {
8
+ let mut lookup = [4; 256];
9
+ lookup[b'A' as usize] = 0;
10
+ lookup[b'C' as usize] = 1;
11
+ lookup[b'G' as usize] = 2;
12
+ lookup[b'T' as usize] = 3;
13
+ lookup[b'a' as usize] = 0;
14
+ lookup[b'c' as usize] = 1;
15
+ lookup[b'g' as usize] = 2;
16
+ lookup[b't' as usize] = 3;
17
+ lookup
18
+ };
19
+
20
+ #[derive(Debug)]
21
+ struct PerfectInterval {
22
+ start: usize,
23
+ finish: usize,
24
+ score: usize,
25
+ l: usize,
26
+ }
27
+
28
+ #[derive(Debug)]
29
+ pub struct SymmetricDust<'a> {
30
+ /// `q` in the paper
31
+ sequence: &'a [u8],
32
+ /// The length of the window used by symmetric DUST algorithm
33
+ /// `W` in the paper
34
+ window_size: usize,
35
+ /// 10 times the score threshold used by symmetric DUST algorithm.
36
+ /// `T` in the paper
37
+ score_threshold: usize,
38
+ /// `P` in the paper
39
+ perfect_intervals: VecDeque<PerfectInterval>,
40
+ /// `res` in the paper
41
+ results: Vec<Range<usize>>,
42
+ /// `w` in the paper
43
+ window: VecDeque<usize>,
44
+ // counts in the current window
45
+ cv: [usize; 64],
46
+ cw: [usize; 64],
47
+ // runnings counts
48
+ rv: usize,
49
+ rw: usize,
50
+ /// `L` in the paper
51
+ biggest_num_triplets: usize,
52
+ }
53
+
54
+ impl<'a> SymmetricDust<'a> {
55
+ pub fn process(
56
+ sequence: &'a [u8],
57
+ window_size: usize,
58
+ score_threshold: usize,
59
+ ) -> Vec<(usize, usize)> {
60
+ let mut obj = SymmetricDust {
61
+ sequence,
62
+ window_size,
63
+ score_threshold,
64
+ perfect_intervals: VecDeque::new(),
65
+ results: Vec::new(),
66
+ window: VecDeque::new(),
67
+ cv: [0; 64],
68
+ cw: [0; 64],
69
+ rv: 0,
70
+ rw: 0,
71
+ biggest_num_triplets: 0,
72
+ };
73
+
74
+ obj.inner_process();
75
+ let mut res = Vec::with_capacity(obj.results.len());
76
+
77
+ // The algorithm can sometimes give end ranges outside of the sequence
78
+ // https://github.com/lh3/sdust/issues/2
79
+ for mut range in obj.results {
80
+ range.end = std::cmp::min(range.end, sequence.len());
81
+ res.push((range.start, range.end));
82
+ }
83
+ res
84
+ }
85
+
86
+ fn inner_process(&mut self) {
87
+ // We're going to represent 3 chars in that u8
88
+ let mut triplet: u8 = 0;
89
+ let mut l = 0;
90
+ for i in 0..=self.sequence.len() {
91
+ let b = if i < self.sequence.len() {
92
+ ENCODING_LOOKUP[self.sequence[i] as usize]
93
+ } else {
94
+ 4
95
+ };
96
+
97
+ // A/T/C/G
98
+ if b < 4 {
99
+ l += 1;
100
+ triplet = (triplet << 2 | b) & MASK;
101
+
102
+ // We have at least 3 chars, we can look at them
103
+ if l >= 3 {
104
+ let mut window_start = if l > self.window_size {
105
+ l - self.window_size
106
+ } else {
107
+ 0
108
+ };
109
+ window_start += i + 1 - l;
110
+
111
+ self.save_masked_regions(window_start);
112
+ self.shift_window(triplet as usize);
113
+ if self.rw * 10 > self.biggest_num_triplets * self.score_threshold {
114
+ self.find_perfect(window_start);
115
+ }
116
+ }
117
+ } else {
118
+ // A `N` resets the sequence
119
+ // When we are there (N or end of seq), we empty the intervals found so far
120
+ let mut window_start = if l > self.window_size - 1 {
121
+ l - self.window_size + 1
122
+ } else {
123
+ 0
124
+ };
125
+ window_start += i + 1 - l;
126
+ while !self.perfect_intervals.is_empty() {
127
+ window_start += 1;
128
+ self.save_masked_regions(window_start);
129
+ }
130
+
131
+ l = 0;
132
+ triplet = 0;
133
+ }
134
+ }
135
+ }
136
+
137
+ /// Save all the intervals that are before the `window_start`
138
+ /// This can only insert one result at a time
139
+ fn save_masked_regions(&mut self, window_start: usize) {
140
+ if self.perfect_intervals.is_empty() {
141
+ return;
142
+ }
143
+
144
+ let back = self.perfect_intervals.back().unwrap();
145
+ if back.start >= window_start {
146
+ return;
147
+ }
148
+
149
+ let num_results = self.results.len();
150
+ // If we already have a result, see if we can merge the last perfect interval with it
151
+ // if they are overlapping
152
+ if num_results > 0 {
153
+ let last_res = &self.results[num_results - 1];
154
+ if back.start <= last_res.end {
155
+ self.results[num_results - 1] =
156
+ last_res.start..std::cmp::max(last_res.end, back.finish);
157
+ } else {
158
+ self.results.push(back.start..back.finish);
159
+ }
160
+ } else {
161
+ self.results.push(back.start..back.finish);
162
+ }
163
+
164
+ while let Some(b) = self.perfect_intervals.back() {
165
+ if b.start < window_start {
166
+ self.perfect_intervals.pop_back();
167
+ } else {
168
+ break;
169
+ }
170
+ }
171
+ }
172
+
173
+ /// Add a triplet to the window, shifting all the data to represent the new window
174
+ fn shift_window(&mut self, triplet: usize) {
175
+ let mut s;
176
+ if self.window.len() >= self.window_size - 2 {
177
+ s = self.window.pop_front().unwrap();
178
+ self.cw[s] -= 1;
179
+ self.rw -= self.cw[s];
180
+ if self.biggest_num_triplets > self.window.len() {
181
+ self.biggest_num_triplets -= 1;
182
+ self.cv[s] -= 1;
183
+ self.rv -= self.cv[s];
184
+ }
185
+ }
186
+
187
+ self.window.push_back(triplet);
188
+ self.biggest_num_triplets += 1;
189
+
190
+ self.rw += self.cw[triplet];
191
+ self.cw[triplet] += 1;
192
+ self.rv += self.cv[triplet];
193
+ self.cv[triplet] += 1;
194
+
195
+ if self.cv[triplet] * 10 > 2 * self.score_threshold {
196
+ loop {
197
+ s = self.window[self.window.len() - self.biggest_num_triplets];
198
+ self.biggest_num_triplets -= 1;
199
+ self.cv[s] -= 1;
200
+ self.rv -= self.cv[s];
201
+
202
+ if s == triplet {
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ /// Find all the perfect intervals in the window
210
+ fn find_perfect(&mut self, window_start: usize) {
211
+ let mut c = self.cv;
212
+ let mut r = self.rv;
213
+ let mut max_score = 0;
214
+ let mut max_l = 0;
215
+
216
+ for i in (0..=self.window.len() - self.biggest_num_triplets - 1).rev() {
217
+ let triplet = self.window[i];
218
+ r += c[triplet];
219
+ c[triplet] += 1;
220
+ let new_score = r;
221
+ let new_l = self.window.len() - i - 1;
222
+ if new_score * 10 > self.score_threshold * new_l {
223
+ let mut insertion_position = 0;
224
+ // Figure out where to insert the new interval
225
+ for (j, interval) in self.perfect_intervals.iter().enumerate() {
226
+ if interval.start < i + window_start {
227
+ break;
228
+ }
229
+ insertion_position = j + 1;
230
+ if max_score == 0 || interval.score * max_l > max_score * interval.l {
231
+ max_score = interval.score;
232
+ max_l = interval.l;
233
+ }
234
+ }
235
+
236
+ // And insert it
237
+ if max_score == 0 || new_score * max_l >= max_score * new_l {
238
+ max_score = new_score;
239
+ max_l = new_l;
240
+ let new_perf = PerfectInterval {
241
+ start: i + window_start,
242
+ // +2 => triplet size (3) - 1
243
+ finish: self.window.len() + 2 + window_start,
244
+ score: new_score,
245
+ l: new_l,
246
+ };
247
+
248
+ self.perfect_intervals.insert(insertion_position, new_perf);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
File without changes
@@ -0,0 +1,42 @@
1
+ from pydustmasker import DustMasker
2
+
3
+
4
+ def test_dust_masker_creation():
5
+ masker = DustMasker("TACCCCCCCGCGTTTTTTT", window_size=64, score_threshold=20)
6
+ assert masker.sequence == "TACCCCCCCGCGTTTTTTT"
7
+ assert masker.window_size == 64
8
+ assert masker.score_threshold == 20
9
+ assert masker.intervals == [(2, 9), (12, 19)]
10
+ assert isinstance(masker.intervals, list)
11
+
12
+
13
+ def test_n_masked_bases():
14
+ masker = DustMasker("TACCCCCCCGCGTTTTTTT", window_size=64, score_threshold=20)
15
+ assert masker.n_masked_bases == 14
16
+
17
+
18
+ def test_mask_soft():
19
+ masker = DustMasker("TACCCCCCCGCGTTTTTTT", window_size=64, score_threshold=20)
20
+ assert masker.mask(hard=False) == "TAcccccccGCGttttttt"
21
+
22
+
23
+ def test_mask_hard():
24
+ masker = DustMasker("TACCCCCCCGCGTTTTTTT", window_size=64, score_threshold=20)
25
+ assert masker.mask(hard=True) == "TANNNNNNNGCGNNNNNNN"
26
+
27
+
28
+ def test_repr():
29
+ masker = DustMasker("GTACCCCCCCGTAACGTTTTT", window_size=64, score_threshold=20)
30
+ assert "DustMasker(sequence: 'GTACCCCC…', intervals: [(3, 10)])" == repr(masker)
31
+
32
+
33
+ def test_window_size():
34
+ masker = DustMasker("TACCCCCCCGCGTTTTTTT", window_size=4, score_threshold=20)
35
+ assert masker.window_size == 4
36
+ assert masker.intervals == []
37
+
38
+
39
+ def test_score_threshold():
40
+ masker = DustMasker("TACCCCCCCGCGTTTTTTT", window_size=64, score_threshold=128)
41
+ assert masker.score_threshold == 128
42
+ assert masker.intervals == []
@@ -0,0 +1,18 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from pydustmasker._version import VERSION
5
+
6
+ if sys.version_info < (3, 11):
7
+ import tomli as tomllib
8
+ else:
9
+ import tomllib
10
+
11
+
12
+ def test_versions_match():
13
+ cargo = Path().absolute() / "Cargo.toml"
14
+ with open(cargo, "rb") as f:
15
+ data = tomllib.load(f)
16
+ cargo_version = data["package"]["version"]
17
+
18
+ assert VERSION == cargo_version