Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-wheels.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

set -euxo pipefail nullglob

for PY in cp38-cp38 cp39-cp39 cp310-cp310; do
for PY in cp310-cp310 cp311-cp311 cp312-cp312; do
PYBIN="/opt/python/${PY}/bin"
"${PYBIN}/pip" install maturin
"${PYBIN}/maturin" build -i "${PYBIN}/python" --release
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v2
with:
python-version: '3.10'
- uses: abatilo/actions-poetry@v2.1.4
python-version: '3.14'
- uses: abatilo/actions-poetry@v3.0.2
- run: poetry install
- run: poetry run black --check --verbose .
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
Expand Down Expand Up @@ -75,7 +75,7 @@ jobs:
- uses: actions/download-artifact@v2 # downloads all artifacts into subdir of current directory named artifact name
- uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: 3.14
- name: Publish
env:
MATURIN_PASSWORD: ${{ secrets.MATURIN_PASSWORD }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v2
with:
python-version: '3.10'
- uses: abatilo/actions-poetry@v2.1.4
python-version: '3.14'
- uses: abatilo/actions-poetry@v3.0.2
- run: poetry install
- run: poetry run maturin develop --release
- run: poetry run pytest
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ publish = false
lazy_static = "1.4.0"
thiserror = "1.0.30"
smallvec = "1.8.0"
pyo3 = {version = "0.20.0", features = ["extension-module"], optional = true}
pyo3 = {version = "0.27.1", features = ["extension-module"], optional = true}
quickcheck = {version = "1.0.3", optional = true}
serde = {version = "1.0", features = ["derive"], optional = true}

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ repository = "https://github.com/SecureDNA/quickdna"


[tool.poetry.dependencies]
python = "^3.8"
python = "^3.10"

[tool.poetry.dev-dependencies]
pytest = "^6.1"
pytest = "^9.0"
pylint = "^2.6"
flake8 = "^3.9"
wheel = "*"
Expand Down
138 changes: 65 additions & 73 deletions src/python_api.rs
Original file line number Diff line number Diff line change
@@ -1,83 +1,75 @@
#![allow(clippy::borrow_deref_ref)] // TODO: broken clippy lint?
// Copyright 2021-2024 SecureDNA Stiftung (SecureDNA Foundation) <licensing@securedna.org>
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright 2021-2024 SecureDNA Stiftung (SecureDNA Foundation) <licensing@securedna.org>
// SPDX-License-Identifier: MIT OR Apache-2.0

use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes};
#[pyo3::pymodule]
mod quickdna {

use crate::{
errors::TranslationError,
trans_table::{reverse_complement_bytes, TranslationTable},
Nucleotide, NucleotideAmbiguous,
};
use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes};

impl From<TranslationError> for PyErr {
fn from(err: TranslationError) -> PyErr {
PyValueError::new_err(err.to_string())
}
}

#[pyfunction]
fn _check_table(table: u8) -> PyResult<()> {
let _ = TranslationTable::try_from(table)?;
Ok(())
}
use crate::{
errors::TranslationError,
trans_table::{reverse_complement_bytes, TranslationTable},
Nucleotide, NucleotideAmbiguous,
};

/// Translate a bytestring of DNA nucleotides into a bytestring of amino acids.
///
/// The input string is allowed to contain IUPAC ambiguity codes; ambiguous
/// codons are represented by `X` in the output.
///
/// * `translate(b"CCNTACACK CATNCNAAT")` returns `b"PYTHXN"`
#[pyfunction]
fn _translate(py: Python, table: u8, dna: &PyBytes) -> PyResult<PyObject> {
let table = TranslationTable::try_from(table)?;
let bytes = table.translate_dna_bytes::<NucleotideAmbiguous>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}
impl From<TranslationError> for PyErr {
fn from(err: TranslationError) -> PyErr {
PyValueError::new_err(err.to_string())
}
}

/// Translate a bytestring of DNA nucleotides into a bytestring of amino acids.
///
/// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes).
///
/// * `translate_strict(b"AAACCCTTTGGG")` returns `b"KPFG"`
/// * `translate_strict(b"AAACCCTTTGGN")` is an error.
#[pyfunction]
fn _translate_strict(py: Python, table: u8, dna: &PyBytes) -> PyResult<PyObject> {
let table = TranslationTable::try_from(table)?;
let bytes = table.translate_dna_bytes::<Nucleotide>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}
#[pyfunction]
fn _check_table(table: u8) -> PyResult<()> {
let _ = TranslationTable::try_from(table)?;
Ok(())
}

/// Get the reverse complement of a bytestring of DNA nucleotides.
///
/// The input string is allowed to contain IUPAC ambiguity codes.
///
/// * `reverse_complement(b"AAAAABCCC")` returns `b"GGGVTTTTT"`
#[pyfunction]
fn _reverse_complement(py: Python, dna: &PyBytes) -> PyResult<PyObject> {
let bytes = reverse_complement_bytes::<NucleotideAmbiguous>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}
/// Translate a bytestring of DNA nucleotides into a bytestring of amino acids.
///
/// The input string is allowed to contain IUPAC ambiguity codes; ambiguous
/// codons are represented by `X` in the output.
///
/// * `translate(b"CCNTACACK CATNCNAAT")` returns `b"PYTHXN"`
#[pyfunction]
fn _translate(py: Python, table: u8, dna: Bound<'_, PyBytes>) -> PyResult<Py<PyAny>> {
let table = TranslationTable::try_from(table)?;
let bytes = table.translate_dna_bytes::<NucleotideAmbiguous>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}

/// Get the reverse complement of a bytestring of DNA nucleotides.
///
/// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes).
///
/// * `reverse_complement_strict(b"AAAAAACCC")` returns `b"GGGTTTTTT"`
/// * `reverse_complement_strict(b"AAAAAACCN")` is an error.
#[pyfunction]
fn _reverse_complement_strict(py: Python, dna: &PyBytes) -> PyResult<PyObject> {
let bytes = reverse_complement_bytes::<Nucleotide>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}
/// Translate a bytestring of DNA nucleotides into a bytestring of amino acids.
///
/// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes).
///
/// * `translate_strict(b"AAACCCTTTGGG")` returns `b"KPFG"`
/// * `translate_strict(b"AAACCCTTTGGN")` is an error.
#[pyfunction]
fn _translate_strict(py: Python, table: u8, dna: Bound<'_, PyBytes>) -> PyResult<Py<PyAny>> {
let table = TranslationTable::try_from(table)?;
let bytes = table.translate_dna_bytes::<Nucleotide>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}

#[pymodule]
fn quickdna(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(_check_table, m)?)?;
m.add_function(wrap_pyfunction!(_translate, m)?)?;
m.add_function(wrap_pyfunction!(_translate_strict, m)?)?;
m.add_function(wrap_pyfunction!(_reverse_complement, m)?)?;
m.add_function(wrap_pyfunction!(_reverse_complement_strict, m)?)?;
/// Get the reverse complement of a bytestring of DNA nucleotides.
///
/// The input string is allowed to contain IUPAC ambiguity codes.
///
/// * `reverse_complement(b"AAAAABCCC")` returns `b"GGGVTTTTT"`
#[pyfunction]
fn _reverse_complement(py: Python, dna: Bound<'_, PyBytes>) -> PyResult<Py<PyAny>> {
let bytes = reverse_complement_bytes::<NucleotideAmbiguous>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}

Ok(())
/// Get the reverse complement of a bytestring of DNA nucleotides.
///
/// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes).
///
/// * `reverse_complement_strict(b"AAAAAACCC")` returns `b"GGGTTTTTT"`
/// * `reverse_complement_strict(b"AAAAAACCN")` is an error.
#[pyfunction]
fn _reverse_complement_strict(py: Python, dna: Bound<'_, PyBytes>) -> PyResult<Py<PyAny>> {
let bytes = reverse_complement_bytes::<Nucleotide>(dna.as_bytes())?;
Ok(PyBytes::new(py, &bytes).into())
}
}
Loading