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.
- pydustmasker-0.1.0/.github/workflows/pypi_publish.yml +158 -0
- pydustmasker-0.1.0/.github/workflows/testing.yml +80 -0
- pydustmasker-0.1.0/.gitignore +141 -0
- pydustmasker-0.1.0/.pre-commit-config.yaml +20 -0
- pydustmasker-0.1.0/CHANGELOG.md +10 -0
- pydustmasker-0.1.0/Cargo.lock +195 -0
- pydustmasker-0.1.0/Cargo.toml +15 -0
- pydustmasker-0.1.0/LICENSE +21 -0
- pydustmasker-0.1.0/PKG-INFO +51 -0
- pydustmasker-0.1.0/README.md +42 -0
- pydustmasker-0.1.0/pydustmasker/__init__.py +7 -0
- pydustmasker-0.1.0/pydustmasker/_pydustmasker.pyi +16 -0
- pydustmasker-0.1.0/pydustmasker/_version.py +1 -0
- pydustmasker-0.1.0/pydustmasker/py.typed +0 -0
- pydustmasker-0.1.0/pyproject.toml +65 -0
- pydustmasker-0.1.0/requirements-dev.txt +7 -0
- pydustmasker-0.1.0/src/lib.rs +142 -0
- pydustmasker-0.1.0/src/sdust.rs +253 -0
- pydustmasker-0.1.0/tests/__init__.py +0 -0
- pydustmasker-0.1.0/tests/test_pydustmasker.py +42 -0
- pydustmasker-0.1.0/tests/test_version.py +18 -0
|
@@ -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,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,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
|