├── .github ├── dependabot.yml └── workflows │ └── test.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── haem.pyi ├── pyproject.toml ├── src ├── aminoacid.rs ├── aminoacidsequence.rs ├── dnabase.rs ├── dnasequence.rs ├── lib.rs ├── member.rs ├── rnabase.rs ├── rnasequence.rs ├── sequence.rs └── utils.rs ├── tests ├── test_amino_acid.py ├── test_amino_acid_sequence.py ├── test_dna_base.py ├── test_dna_sequence.py ├── test_rna_base.py └── test_rna_sequence.py └── uv.lock /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: { interval: weekly } 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: { interval: weekly } 9 | - package-ecosystem: pip 10 | directory: / 11 | schedule: { interval: weekly } 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["*"] 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: { contents: read } 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | python-version: ["3.10", "3.11", "3.12", "3.13"] 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v5 21 | - uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cargo/bin/ 25 | ~/.cargo/registry/index/ 26 | ~/.cargo/registry/cache/ 27 | ~/.cargo/git/db/ 28 | target/ 29 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 30 | - uses: actions/setup-python@v6 31 | with: { python-version: "${{ matrix.python-version }}" } 32 | - uses: astral-sh/setup-uv@v7 33 | with: { version: "latest" } 34 | - run: uv sync --extra=dev 35 | - uses: PyO3/maturin-action@v1 36 | with: { args: "--release" } 37 | - run: make test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.5.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 10 | 11 | [[package]] 12 | name = "crossbeam-deque" 13 | version = "0.8.6" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 16 | dependencies = [ 17 | "crossbeam-epoch", 18 | "crossbeam-utils", 19 | ] 20 | 21 | [[package]] 22 | name = "crossbeam-epoch" 23 | version = "0.9.18" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 26 | dependencies = [ 27 | "crossbeam-utils", 28 | ] 29 | 30 | [[package]] 31 | name = "crossbeam-utils" 32 | version = "0.8.21" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 35 | 36 | [[package]] 37 | name = "either" 38 | version = "1.15.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 41 | 42 | [[package]] 43 | name = "haem" 44 | version = "0.1.0" 45 | dependencies = [ 46 | "pyo3", 47 | "rayon", 48 | ] 49 | 50 | [[package]] 51 | name = "heck" 52 | version = "0.5.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 55 | 56 | [[package]] 57 | name = "indoc" 58 | version = "2.0.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 61 | 62 | [[package]] 63 | name = "libc" 64 | version = "0.2.176" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 67 | 68 | [[package]] 69 | name = "memoffset" 70 | version = "0.9.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 73 | dependencies = [ 74 | "autocfg", 75 | ] 76 | 77 | [[package]] 78 | name = "once_cell" 79 | version = "1.21.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 82 | 83 | [[package]] 84 | name = "portable-atomic" 85 | version = "1.11.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 88 | 89 | [[package]] 90 | name = "proc-macro2" 91 | version = "1.0.101" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 94 | dependencies = [ 95 | "unicode-ident", 96 | ] 97 | 98 | [[package]] 99 | name = "pyo3" 100 | version = "0.26.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" 103 | dependencies = [ 104 | "indoc", 105 | "libc", 106 | "memoffset", 107 | "once_cell", 108 | "portable-atomic", 109 | "pyo3-build-config", 110 | "pyo3-ffi", 111 | "pyo3-macros", 112 | "unindent", 113 | ] 114 | 115 | [[package]] 116 | name = "pyo3-build-config" 117 | version = "0.26.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" 120 | dependencies = [ 121 | "target-lexicon", 122 | ] 123 | 124 | [[package]] 125 | name = "pyo3-ffi" 126 | version = "0.26.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" 129 | dependencies = [ 130 | "libc", 131 | "pyo3-build-config", 132 | ] 133 | 134 | [[package]] 135 | name = "pyo3-macros" 136 | version = "0.26.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" 139 | dependencies = [ 140 | "proc-macro2", 141 | "pyo3-macros-backend", 142 | "quote", 143 | "syn", 144 | ] 145 | 146 | [[package]] 147 | name = "pyo3-macros-backend" 148 | version = "0.26.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" 151 | dependencies = [ 152 | "heck", 153 | "proc-macro2", 154 | "pyo3-build-config", 155 | "quote", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "quote" 161 | version = "1.0.41" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 164 | dependencies = [ 165 | "proc-macro2", 166 | ] 167 | 168 | [[package]] 169 | name = "rayon" 170 | version = "1.11.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 173 | dependencies = [ 174 | "either", 175 | "rayon-core", 176 | ] 177 | 178 | [[package]] 179 | name = "rayon-core" 180 | version = "1.13.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 183 | dependencies = [ 184 | "crossbeam-deque", 185 | "crossbeam-utils", 186 | ] 187 | 188 | [[package]] 189 | name = "syn" 190 | version = "2.0.106" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote", 196 | "unicode-ident", 197 | ] 198 | 199 | [[package]] 200 | name = "target-lexicon" 201 | version = "0.13.3" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" 204 | 205 | [[package]] 206 | name = "unicode-ident" 207 | version = "1.0.19" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 210 | 211 | [[package]] 212 | name = "unindent" 213 | version = "0.2.4" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" 216 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "haem" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "haem" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | pyo3 = "0.26" 12 | rayon = "1" 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: check 2 | uv run maturin develop --release 3 | uv run pytest . 4 | 5 | check: 6 | cargo fmt --check 7 | cargo clippy 8 | uv run ruff check 9 | uv run ruff format --check 10 | uv run mypy --strict . 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # haem 2 | 3 | ## Quick start 4 | 5 | Create a DNA sequence, complement and transcribe it: 6 | 7 | ```python 8 | >>> import haem 9 | >>> dna = haem.DNASequence("ACGT") 10 | >>> dna.complement 11 | 12 | >>> dna.transcribe() 13 | 14 | ``` 15 | 16 | Create an amino acid from a codon and from an ambiguous codon: 17 | 18 | ```python 19 | >>> haem.AminoAcid("UCA") 20 | AminoAcid.SERINE 21 | >>> haem.AminoAcid("UCN") 22 | AminoAcid.SERINE 23 | ``` 24 | -------------------------------------------------------------------------------- /haem.pyi: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | class StopTranslation(Exception): 4 | """StopTranslation is raised when a stop codon is encountered during 5 | translation.""" 6 | 7 | ... 8 | 9 | class DNABase: 10 | """An enumeration of DNA bases, as defined by IUPAC. 11 | 12 | DNABases may be instantiated either directly by their variant or by their 13 | IUPAC code. For example: 14 | 15 | >>> DNABase.THYMINE 16 | >>> DNABase('T') 17 | 18 | A ValueError is raised if the code is not valid.""" 19 | 20 | ADENINE: DNABase 21 | CYTOSINE: DNABase 22 | GUANINE: DNABase 23 | THYMINE: DNABase 24 | ADENINE_CYTOSINE: DNABase 25 | ADENINE_GUANINE: DNABase 26 | ADENINE_THYMINE: DNABase 27 | CYTOSINE_GUANINE: DNABase 28 | CYTOSINE_THYMINE: DNABase 29 | GUANINE_THYMINE: DNABase 30 | ADENINE_CYTOSINE_GUANINE: DNABase 31 | ADENINE_CYTOSINE_THYMINE: DNABase 32 | ADENINE_GUANINE_THYMINE: DNABase 33 | CYTOSINE_GUANINE_THYMINE: DNABase 34 | ANY: DNABase 35 | GAP: DNABase 36 | 37 | @classmethod 38 | def __new__(cls, code: str) -> DNABase: ... 39 | @property 40 | def code(self) -> str: 41 | """One-letter IUPAC code of the DNA base.""" 42 | ... 43 | 44 | @property 45 | def complement(self) -> DNABase: 46 | """The complementary DNA base.""" 47 | ... 48 | 49 | def transcribe(self) -> RNABase: 50 | """Transcription of the DNA base to a RNA base.""" 51 | ... 52 | 53 | def __repr__(self) -> str: ... 54 | def __str__(self) -> str: ... 55 | def __eq__(self, other: object) -> bool: ... 56 | def __ne__(self, other: object) -> bool: ... 57 | def __bool__(self) -> bool: 58 | """Casting to bool is False for DNABase.GAP and True otherwise.""" 59 | ... 60 | 61 | def __invert__(self) -> DNABase: 62 | """See `DNABase.complement`.""" 63 | 64 | def __add__( 65 | self, 66 | other: typing.Union[ 67 | DNABase, 68 | DNASequence, 69 | str, 70 | typing.Iterator[typing.Union[str | DNABase]], 71 | typing.Sequence[typing.Union[str | DNABase]], 72 | str, 73 | ], 74 | ) -> DNASequence: 75 | """Create a new sequence consisting of this base followed by the given 76 | sequence member(s).""" 77 | ... 78 | 79 | def __radd__( 80 | self, 81 | other: typing.Union[ 82 | DNABase, 83 | DNASequence, 84 | str, 85 | typing.Iterator[typing.Union[str | DNABase]], 86 | typing.Sequence[typing.Union[str | DNABase]], 87 | str, 88 | ], 89 | ) -> DNASequence: 90 | """Create a new sequence consisting of the given sequence member(s) 91 | followed by this base.""" 92 | ... 93 | 94 | class RNABase: 95 | """An enumeration of RNA bases, as defined by IUPAC. 96 | 97 | RNABases may be instantiated either directly by their variant or by their 98 | IUPAC code. For example: 99 | 100 | >>> RNABase.URACIL 101 | >>> RNABase('U') 102 | 103 | A ValueError is raised if the code is not valid.""" 104 | 105 | ADENINE: RNABase 106 | CYTOSINE: RNABase 107 | GUANINE: RNABase 108 | URACIL: RNABase 109 | ADENINE_CYTOSINE: RNABase 110 | ADENINE_GUANINE: RNABase 111 | ADENINE_URACIL: RNABase 112 | CYTOSINE_GUANINE: RNABase 113 | CYTOSINE_URACIL: RNABase 114 | GUANINE_URACIL: RNABase 115 | ADENINE_CYTOSINE_GUANINE: RNABase 116 | ADENINE_CYTOSINE_URACIL: RNABase 117 | ADENINE_GUANINE_URACIL: RNABase 118 | CYTOSINE_GUANINE_URACIL: RNABase 119 | ANY: RNABase 120 | GAP: RNABase 121 | 122 | @classmethod 123 | def __new__(cls, code: str) -> RNABase: ... 124 | @property 125 | def code(self) -> str: 126 | """One-letter IUPAC code of the RNA base.""" 127 | ... 128 | 129 | @property 130 | def complement(self) -> RNABase: 131 | """The complementary RNA base.""" 132 | ... 133 | 134 | def retro_transcribe(self) -> DNABase: 135 | """Reverse transcription of the RNA base to a DNA base.""" 136 | ... 137 | 138 | def __repr__(self) -> str: ... 139 | def __str__(self) -> str: ... 140 | def __eq__(self, other: object) -> bool: ... 141 | def __ne__(self, other: object) -> bool: ... 142 | def __bool__(self) -> bool: 143 | """Casting to bool is False for RNABase.GAP and True otherwise.""" 144 | ... 145 | 146 | def __invert__(self) -> RNABase: 147 | """See `RNABase.complement`.""" 148 | 149 | def __add__( 150 | self, 151 | other: typing.Union[ 152 | RNABase, 153 | RNASequence, 154 | typing.Iterator[typing.Union[str | RNABase]], 155 | typing.Sequence[typing.Union[str | RNABase]], 156 | str, 157 | ], 158 | ) -> RNASequence: 159 | """Create a new sequence consisting of this base followed by the given 160 | sequence member(s).""" 161 | ... 162 | 163 | def __radd__( 164 | self, 165 | other: typing.Union[ 166 | RNABase, 167 | RNASequence, 168 | typing.Iterator[typing.Union[str | RNABase]], 169 | typing.Sequence[typing.Union[str | RNABase]], 170 | str, 171 | ], 172 | ) -> RNASequence: 173 | """Create a new sequence consisting of the given sequence member(s) 174 | followed by this base.""" 175 | ... 176 | 177 | class AminoAcid: 178 | """An enumeration of amino acids, as defined by IUPAC. 179 | 180 | AminoAcids may be instantiated either directly by their variant, by their 181 | IUPAC code, by a tuple of three RNABases or by a string containing three 182 | RNABase IUPAC codes. For example: 183 | 184 | >>> AminoAcid.SERINE 185 | >>> AminoAcid('S') 186 | >>> AminoAcid((RNABase.ADENINE, RNABase.GUANINE, RNABase.CYTOSINE)) 187 | >>> AminoAcid(('A', 'G', 'C')) 188 | >>> AminoAcid('AGC') 189 | 190 | AminoAcids may also be instantiated by ambiguous IUPAC RNA codes where 191 | appropriate. For example: 192 | 193 | >>> AminoAcid('AGY') # Serine 194 | 195 | Invalid inputs or codons that result in ambiguous amino acids will raise a 196 | ValueError. 197 | 198 | Stop codons will cause a StopTranslation exception to be raised.""" 199 | 200 | ALANINE: AminoAcid 201 | ASPARTIC_ACID_ASPARAGINE: AminoAcid 202 | CYSTEINE: AminoAcid 203 | ASPARTIC_ACID: AminoAcid 204 | GLUTAMIC_ACID: AminoAcid 205 | PHENYLALANINE: AminoAcid 206 | GLYCINE: AminoAcid 207 | HISTIDINE: AminoAcid 208 | ISOLEUCINE: AminoAcid 209 | LYSINE: AminoAcid 210 | LEUCINE: AminoAcid 211 | METHIONINE: AminoAcid 212 | ASPARAGINE: AminoAcid 213 | PROLINE: AminoAcid 214 | GLUTAMINE: AminoAcid 215 | ARGININE: AminoAcid 216 | SERINE: AminoAcid 217 | THREONINE: AminoAcid 218 | VALINE: AminoAcid 219 | TRYPTOPHAN: AminoAcid 220 | ANY: AminoAcid 221 | TYROSINE: AminoAcid 222 | GLUTAMINE_GLUTAMIC_ACID: AminoAcid 223 | 224 | @classmethod 225 | def __new__( 226 | cls, 227 | code_or_codon: typing.Union[ 228 | str, typing.Tuple[RNABase, RNABase, RNABase], typing.Tuple[str, str, str] 229 | ], 230 | ) -> AminoAcid: ... 231 | @property 232 | def code(self) -> str: 233 | """One-letter IUPAC code of the amino acid.""" 234 | ... 235 | 236 | @property 237 | def short_name(self) -> str: 238 | """Three-letter IUPAC code of the amino acid.""" 239 | ... 240 | 241 | def __repr__(self) -> str: ... 242 | def __str__(self) -> str: ... 243 | def __eq__(self, other: object) -> bool: ... 244 | def __ne__(self, other: object) -> bool: ... 245 | def __bool__(self) -> bool: 246 | """Always true.""" 247 | ... 248 | 249 | def __add__( 250 | self, other: typing.Union[AminoAcid, AminoAcidSequence, str] 251 | ) -> AminoAcidSequence: 252 | """Create a new sequence consisting of this amino acid followed by the 253 | given sequence member(s).""" 254 | ... 255 | 256 | def __radd__( 257 | self, other: typing.Union[AminoAcid, AminoAcidSequence, str] 258 | ) -> AminoAcidSequence: 259 | """Create a new sequence consisting of the given sequence member(s) 260 | followed by this amino acid.""" 261 | ... 262 | 263 | class DNASequence: 264 | @classmethod 265 | def __new__( 266 | cls, 267 | bases: typing.Union[ 268 | str, 269 | typing.Iterable[typing.Union[str, DNABase]], 270 | typing.Sequence[typing.Union[str, DNABase]], 271 | ] = "", 272 | ) -> DNASequence: 273 | """A sequence of `DNABase`s. 274 | 275 | `DNASequence` may be instantiated by a string of IUPAC DNA codes, or a 276 | sequence or iterable of `DNABase`s or IUPAC DNA codes. For example: 277 | 278 | >>> DNASequence("ACGT") 279 | >>> DNASequence(["A", "C", "G", "T"]) 280 | >>> DNASequence(iter(["A", "C", "G", "T"])) 281 | >>> DNASequence([DNABase.ADENINE, DNABase.CYTOSINE]) 282 | >>> DNASequence(iter([DNABase.ADENINE, DNABase.CYTOSINE])) 283 | 284 | A ValueError is raised if any DNA code is not valid.""" 285 | 286 | ... 287 | 288 | @property 289 | def complement(self) -> DNASequence: 290 | """The complementary DNA sequence.""" 291 | 292 | def transcribe(self) -> RNASequence: 293 | """Transcription of the DNA sequence to a RNA sequence.""" 294 | ... 295 | 296 | def __invert__(self) -> DNASequence: 297 | """See `DNASequence.complement`.""" 298 | ... 299 | 300 | def __repr__(self) -> str: ... 301 | def __str__(self) -> str: ... 302 | def __eq__(self, other: object) -> bool: ... 303 | def __ne__(self, other: object) -> bool: ... 304 | def __bool__(self) -> bool: 305 | """Casting to bool is False for empty sequences and True otherwise.""" 306 | ... 307 | 308 | def __add__(self, other: typing.Union[DNABase, DNASequence, str]) -> DNASequence: 309 | """Create a new sequence consisting of this sequence followed by the 310 | given sequence member(s).""" 311 | 312 | def __radd__(self, other: typing.Union[DNABase, DNASequence, str]) -> DNASequence: 313 | """Create a new sequence consisting of the given sequence member(s) 314 | followed by this sequence.""" 315 | ... 316 | 317 | def __contains__(self, item: typing.Union[DNABase, DNASequence]) -> bool: 318 | """Return true if the given DNABase or DNASequence is contained within 319 | this sequence.""" 320 | ... 321 | 322 | def __len__(self) -> int: ... 323 | def __getitem__( 324 | self, key: typing.Union[int, slice] 325 | ) -> typing.Union[DNABase, DNASequence]: ... 326 | def __iter__(self) -> typing.Iterator[DNABase]: ... 327 | def count( 328 | self, item: typing.Union[DNABase, DNASequence, str], overlap: bool = True 329 | ) -> int: 330 | """Count the occurances of a DNABase in the sequence.""" 331 | ... 332 | 333 | def find( 334 | self, target: typing.Union[DNASequence, DNABase, str] 335 | ) -> typing.Optional[int]: 336 | """Find the index of the first occurance of the given DNABase or 337 | DNASequence.""" 338 | ... 339 | 340 | class RNASequence: 341 | @classmethod 342 | def __new__( 343 | cls, 344 | bases: typing.Union[ 345 | str, 346 | typing.Iterable[typing.Union[str, RNABase]], 347 | typing.Sequence[typing.Union[str, RNABase]], 348 | ] = "", 349 | ) -> RNASequence: 350 | """A sequence of `RNABase`s. 351 | 352 | `RNASequence` may be instantiated by a string of IUPAC RNA codes, or a 353 | sequence or iterable of `RNABase`s or IUPAC RNA codes. For example: 354 | 355 | >>> RNASequence("ACGU") 356 | >>> RNASequence(["A", "C", "G", "U"]) 357 | >>> RNASequence(iter(["A", "C", "G", "U"])) 358 | >>> RNASequence([RNABase.ADENINE, RNABase.CYTOSINE]) 359 | >>> RNASequence(iter([RNABase.ADENINE, RNABase.CYTOSINE])) 360 | 361 | A ValueError is raised if any RNA code is not valid.""" 362 | 363 | ... 364 | 365 | @property 366 | def complement(self) -> RNASequence: 367 | """The complementary RNA sequence.""" 368 | 369 | def retro_transcribe(self) -> DNASequence: 370 | """Reverse transcription of the RNA sequence to a DNA sequence.""" 371 | ... 372 | 373 | def translate(self) -> AminoAcidSequence: 374 | """Translate the RNA sequence to an amino acid sequence. 375 | 376 | Translation searches for the first Methionine (AUG) codon and 377 | translates until it finds a stop codon. 378 | 379 | A ValueError is raised if no start codon is found, or not stop codon is 380 | found following the start codon.""" 381 | ... 382 | 383 | def __invert__(self) -> RNASequence: 384 | """See `RNASequence.complement`.""" 385 | ... 386 | 387 | def __repr__(self) -> str: ... 388 | def __str__(self) -> str: ... 389 | def __eq__(self, other: object) -> bool: ... 390 | def __ne__(self, other: object) -> bool: ... 391 | def __bool__(self) -> bool: 392 | """Casting to bool is False for empty sequences and True otherwise.""" 393 | ... 394 | 395 | def __add__(self, other: typing.Union[RNABase, RNASequence, str]) -> RNASequence: 396 | """Create a new sequence consisting of this sequence followed by the 397 | given sequence member(s).""" 398 | 399 | def __radd__(self, other: typing.Union[RNABase, RNASequence, str]) -> RNASequence: 400 | """Create a new sequence consisting of the given sequence member(s) 401 | followed by this sequence.""" 402 | ... 403 | 404 | def __contains__(self, item: typing.Union[RNABase, RNASequence]) -> bool: 405 | """Return true if the given RNABase or RNASequence is contained within 406 | this sequence.""" 407 | ... 408 | 409 | def __len__(self) -> int: ... 410 | def __getitem__( 411 | self, key: typing.Union[int, slice] 412 | ) -> typing.Union[RNABase, RNASequence]: ... 413 | def __iter__(self) -> typing.Iterator[RNABase]: ... 414 | def count( 415 | self, item: typing.Union[RNABase, RNASequence, str], overlap: bool = True 416 | ) -> int: 417 | """Count the occurances of a RNABase in the sequence.""" 418 | ... 419 | 420 | def find( 421 | self, target: typing.Union[RNASequence, RNABase, str] 422 | ) -> typing.Optional[int]: 423 | """Find the index of the first occurance of the given RNABase or 424 | RNASequence.""" 425 | ... 426 | 427 | class AminoAcidSequence: 428 | @classmethod 429 | def __new__( 430 | cls, 431 | bases: typing.Union[ 432 | str, 433 | typing.Iterable[typing.Union[str, AminoAcid]], 434 | typing.Sequence[typing.Union[str, AminoAcid]], 435 | ] = "", 436 | ) -> AminoAcidSequence: 437 | """A sequence of `AminoAcid`s. 438 | 439 | `AminoAcidSequence` may be instantiated by a string of IUPAC amino acid 440 | codes, or a sequence or iterable of `AminoAcid`s or IUPAC amino acid 441 | codes. For example: 442 | 443 | >>> AminoAcidSequence("MVVR") 444 | >>> AminoAcidSequence(["M", "V", "V", "R"]) 445 | >>> AminoAcidSequence(iter(["M", "V", "V", "R"])) 446 | >>> AminoAcidSequence([AminoAcid.METHIONINE, AminoAcid.VALINE]) 447 | >>> AminoAcidSequence(iter([AminoAcid.METHIONINE, AminoAcid.VALINE])) 448 | 449 | A ValueError is raised if any amino acid code is not valid.""" 450 | 451 | ... 452 | 453 | def __repr__(self) -> str: ... 454 | def __str__(self) -> str: ... 455 | def __eq__(self, other: object) -> bool: ... 456 | def __ne__(self, other: object) -> bool: ... 457 | def __bool__(self) -> bool: 458 | """Casting to bool is False for empty sequences and True otherwise.""" 459 | ... 460 | 461 | def __add__( 462 | self, other: typing.Union[AminoAcid, AminoAcidSequence, str] 463 | ) -> AminoAcidSequence: 464 | """Create a new sequence consisting of this sequence followed by the 465 | given sequence member(s).""" 466 | 467 | def __radd__( 468 | self, other: typing.Union[AminoAcidSequence, AminoAcid, str] 469 | ) -> AminoAcidSequence: 470 | """Create a new sequence consisting of the given sequence member(s) 471 | followed by this sequence.""" 472 | ... 473 | 474 | def __contains__(self, item: typing.Union[AminoAcid, AminoAcidSequence]) -> bool: 475 | """Return true if the given AminoAcid or AminoAcidSequence is contained 476 | within this sequence.""" 477 | ... 478 | 479 | def __len__(self) -> int: ... 480 | def __getitem__( 481 | self, key: typing.Union[int, slice] 482 | ) -> typing.Union[AminoAcid, AminoAcidSequence]: ... 483 | def __iter__(self) -> typing.Iterator[AminoAcid]: ... 484 | def count( 485 | self, 486 | item: typing.Union[AminoAcid, str, AminoAcidSequence], 487 | overlap: bool = False, 488 | ) -> int: 489 | """Count the occurances of an AminoAcid in the sequence.""" 490 | ... 491 | 492 | def find( 493 | self, target: typing.Union[AminoAcidSequence, AminoAcid, str] 494 | ) -> typing.Optional[int]: 495 | """Find the index of the first occurance of the given AminoAcid or 496 | AminoAcidSequence.""" 497 | ... 498 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [{name = "Tom Godkin", email = "tomgodkin@pm.me"}] 3 | classifiers = [ 4 | "Programming Language :: Rust", 5 | "Programming Language :: Python :: Implementation :: CPython", 6 | "Programming Language :: Python :: Implementation :: PyPy", 7 | ] 8 | description = "A Python library for working on Bioinformatics problems." 9 | name = "haem" 10 | readme = "README.md" 11 | requires-python = ">= 3.9" 12 | version = "0.1.0" 13 | 14 | [build-system] 15 | build-backend = "maturin" 16 | requires = ["maturin>=1.1,<2.0"] 17 | 18 | [tool.maturin] 19 | features = ["pyo3/extension-module"] 20 | 21 | [project.optional-dependencies] 22 | dev = ["maturin>=1.8", "mypy>=1.14", "pytest>=8.3", "ruff>=0.9"] 23 | 24 | [tool.ruff.lint] 25 | extend-select = ["I"] 26 | isort.known-first-party = ["haem"] 27 | -------------------------------------------------------------------------------- /src/aminoacid.rs: -------------------------------------------------------------------------------- 1 | use crate::aminoacidsequence::AminoAcidSequence; 2 | use crate::aminoacidsequence::AminoAcidSequenceInput; 3 | use crate::member::Member; 4 | use crate::rnabase::RNABase; 5 | use pyo3::create_exception; 6 | use pyo3::prelude::*; 7 | use pyo3::pybacked::PyBackedStr; 8 | use std::fmt; 9 | 10 | create_exception!(haem, StopTranslation, pyo3::exceptions::PyException); 11 | 12 | #[derive(FromPyObject)] 13 | enum Codon { 14 | Bases(RNABase, RNABase, RNABase), 15 | Chars(char, char, char), 16 | } 17 | 18 | impl TryFrom for AminoAcid { 19 | type Error = PyErr; 20 | 21 | fn try_from(codon: Codon) -> PyResult { 22 | match codon { 23 | Codon::Bases(first, second, third) => (&first, &second, &third).try_into(), 24 | Codon::Chars(first, second, third) => (first, second, third).try_into(), 25 | } 26 | } 27 | } 28 | 29 | impl TryFrom<(char, char, char)> for AminoAcid { 30 | type Error = PyErr; 31 | 32 | fn try_from(codon: (char, char, char)) -> PyResult { 33 | ( 34 | &RNABase::try_from(codon.0)?, 35 | &RNABase::try_from(codon.1)?, 36 | &RNABase::try_from(codon.2)?, 37 | ) 38 | .try_into() 39 | } 40 | } 41 | 42 | #[derive(FromPyObject)] 43 | enum CodeOrCodon { 44 | Code(char), 45 | Codon(Codon), 46 | CodonStr(PyBackedStr), 47 | } 48 | 49 | impl TryFrom for AminoAcid { 50 | type Error = PyErr; 51 | 52 | fn try_from(code_or_codon: CodeOrCodon) -> PyResult { 53 | Ok(match code_or_codon { 54 | CodeOrCodon::Code(code) => code.try_into()?, 55 | CodeOrCodon::Codon(codon) => codon.try_into()?, 56 | CodeOrCodon::CodonStr(codon) if codon.len() == 3 => { 57 | let bases = codon 58 | .chars() 59 | .map(RNABase::try_from) 60 | .collect::>>()?; 61 | 62 | (&bases[0], &bases[1], &bases[2]).try_into()? 63 | } 64 | _ => { 65 | return Err(pyo3::exceptions::PyValueError::new_err( 66 | "invalid amino acid codon", 67 | )) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | #[pyclass(eq, eq_int, frozen, rename_all = "SCREAMING_SNAKE_CASE")] 74 | #[derive(Clone, Copy, PartialEq, Debug)] 75 | pub enum AminoAcid { 76 | Alanine, 77 | AsparticAcidAsparagine, 78 | Cysteine, 79 | AsparticAcid, 80 | GlutamicAcid, 81 | Phenylalanine, 82 | Glycine, 83 | Histidine, 84 | Isoleucine, 85 | Lysine, 86 | Leucine, 87 | Methionine, 88 | Asparagine, 89 | Proline, 90 | Glutamine, 91 | Arginine, 92 | Serine, 93 | Threonine, 94 | Valine, 95 | Tryptophan, 96 | Any, 97 | Tyrosine, 98 | GlutamineGlutamicAcid, 99 | } 100 | 101 | impl Member for AminoAcid {} 102 | 103 | #[pymethods] 104 | impl AminoAcid { 105 | #[new] 106 | fn __new__(code_or_codon: CodeOrCodon) -> PyResult { 107 | code_or_codon.try_into() 108 | } 109 | 110 | #[getter] 111 | fn get_code(&self) -> char { 112 | self.into() 113 | } 114 | 115 | #[getter] 116 | fn get_short_name(&self) -> &'static str { 117 | match self { 118 | Self::Alanine => "ala", 119 | Self::AsparticAcidAsparagine => "asx", 120 | Self::Cysteine => "cys", 121 | Self::AsparticAcid => "asp", 122 | Self::GlutamicAcid => "glu", 123 | Self::Phenylalanine => "phe", 124 | Self::Glycine => "gly", 125 | Self::Histidine => "his", 126 | Self::Isoleucine => "ile", 127 | Self::Lysine => "lys", 128 | Self::Leucine => "leu", 129 | Self::Methionine => "met", 130 | Self::Asparagine => "asn", 131 | Self::Proline => "pro", 132 | Self::Glutamine => "gln", 133 | Self::Arginine => "arg", 134 | Self::Serine => "ser", 135 | Self::Threonine => "thr", 136 | Self::Valine => "val", 137 | Self::Tryptophan => "trp", 138 | Self::Any => "xaa", 139 | Self::Tyrosine => "tyr", 140 | Self::GlutamineGlutamicAcid => "glx", 141 | } 142 | } 143 | 144 | fn __str__(&self) -> String { 145 | self.to_string() 146 | } 147 | 148 | fn __bool__(&self) -> bool { 149 | true 150 | } 151 | 152 | fn __add__(&self, other: AminoAcidSequenceInput) -> PyResult { 153 | Ok(AminoAcidSequence { 154 | sequence: self.add(&AminoAcidSequence::try_from(other)?, false), 155 | }) 156 | } 157 | 158 | fn __radd__(&self, other: AminoAcidSequenceInput) -> PyResult { 159 | Ok(AminoAcidSequence { 160 | sequence: self.add(&AminoAcidSequence::try_from(other)?, true), 161 | }) 162 | } 163 | } 164 | 165 | impl From<&AminoAcid> for char { 166 | fn from(amino_acid: &AminoAcid) -> Self { 167 | match amino_acid { 168 | AminoAcid::Alanine => 'A', 169 | AminoAcid::AsparticAcidAsparagine => 'B', 170 | AminoAcid::Cysteine => 'C', 171 | AminoAcid::AsparticAcid => 'D', 172 | AminoAcid::GlutamicAcid => 'E', 173 | AminoAcid::Phenylalanine => 'F', 174 | AminoAcid::Glycine => 'G', 175 | AminoAcid::Histidine => 'H', 176 | AminoAcid::Isoleucine => 'I', 177 | AminoAcid::Lysine => 'K', 178 | AminoAcid::Leucine => 'L', 179 | AminoAcid::Methionine => 'M', 180 | AminoAcid::Asparagine => 'N', 181 | AminoAcid::Proline => 'P', 182 | AminoAcid::Glutamine => 'Q', 183 | AminoAcid::Arginine => 'R', 184 | AminoAcid::Serine => 'S', 185 | AminoAcid::Threonine => 'T', 186 | AminoAcid::Valine => 'V', 187 | AminoAcid::Tryptophan => 'W', 188 | AminoAcid::Any => 'X', 189 | AminoAcid::Tyrosine => 'Y', 190 | AminoAcid::GlutamineGlutamicAcid => 'Z', 191 | } 192 | } 193 | } 194 | 195 | impl fmt::Display for AminoAcid { 196 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 197 | write!( 198 | f, 199 | "{}", 200 | match self { 201 | Self::Alanine => "alanine", 202 | Self::AsparticAcidAsparagine => "aspartic acid/asparagine", 203 | Self::Cysteine => "cysteine", 204 | Self::AsparticAcid => "aspartic acid", 205 | Self::GlutamicAcid => "glutamic acid", 206 | Self::Phenylalanine => "phenylalanine", 207 | Self::Glycine => "glycine", 208 | Self::Histidine => "histidine", 209 | Self::Isoleucine => "isoleucine", 210 | Self::Lysine => "lysine", 211 | Self::Leucine => "leucine", 212 | Self::Methionine => "methionine", 213 | Self::Asparagine => "asparagine", 214 | Self::Proline => "proline", 215 | Self::Glutamine => "glutamine", 216 | Self::Arginine => "arginine", 217 | Self::Serine => "serine", 218 | Self::Threonine => "threonine", 219 | Self::Valine => "valine", 220 | Self::Tryptophan => "tryptophan", 221 | Self::Any => "any", 222 | Self::Tyrosine => "tyrosine", 223 | Self::GlutamineGlutamicAcid => "glutamine/glutamic acid", 224 | } 225 | ) 226 | } 227 | } 228 | 229 | impl TryFrom for AminoAcid { 230 | type Error = PyErr; 231 | 232 | fn try_from(code: char) -> PyResult { 233 | Ok(match code { 234 | 'A' => Self::Alanine, 235 | 'B' => Self::AsparticAcidAsparagine, 236 | 'C' => Self::Cysteine, 237 | 'D' => Self::AsparticAcid, 238 | 'E' => Self::GlutamicAcid, 239 | 'F' => Self::Phenylalanine, 240 | 'G' => Self::Glycine, 241 | 'H' => Self::Histidine, 242 | 'I' => Self::Isoleucine, 243 | 'K' => Self::Lysine, 244 | 'L' => Self::Leucine, 245 | 'M' => Self::Methionine, 246 | 'N' => Self::Asparagine, 247 | 'P' => Self::Proline, 248 | 'Q' => Self::Glutamine, 249 | 'R' => Self::Arginine, 250 | 'S' => Self::Serine, 251 | 'T' => Self::Threonine, 252 | 'V' => Self::Valine, 253 | 'W' => Self::Tryptophan, 254 | 'X' => Self::Any, 255 | 'Y' => Self::Tyrosine, 256 | 'Z' => Self::GlutamineGlutamicAcid, 257 | _ => { 258 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 259 | "invalid IUPAC amino acid code \"{code}\"" 260 | ))) 261 | } 262 | }) 263 | } 264 | } 265 | 266 | impl TryFrom<(&RNABase, &RNABase, &RNABase)> for AminoAcid { 267 | type Error = PyErr; 268 | 269 | fn try_from(codon: (&RNABase, &RNABase, &RNABase)) -> PyResult { 270 | Ok(match (codon.0, codon.1, codon.2) { 271 | (RNABase::Gap, _, _) | (_, RNABase::Gap, _) | (_, _, RNABase::Gap) => { 272 | return Err(pyo3::exceptions::PyValueError::new_err( 273 | "codon contains gap", 274 | )) 275 | } 276 | 277 | // Alanine 278 | (RNABase::Guanine, RNABase::Cytosine, _) => Self::Alanine, 279 | 280 | // Aspartic acid/Asparagine 281 | ( 282 | RNABase::AdenineGuanine, 283 | RNABase::Adenine, 284 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 285 | ) => Self::AsparticAcidAsparagine, 286 | 287 | // Cysteine 288 | ( 289 | RNABase::Uracil, 290 | RNABase::Guanine, 291 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 292 | ) => Self::Cysteine, 293 | 294 | // Aspartic acid 295 | ( 296 | RNABase::Guanine, 297 | RNABase::Adenine, 298 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 299 | ) => Self::AsparticAcid, 300 | 301 | // Glutamic acid 302 | ( 303 | RNABase::Guanine, 304 | RNABase::Adenine, 305 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 306 | ) => Self::GlutamicAcid, 307 | 308 | // Phenylalanine 309 | ( 310 | RNABase::Uracil, 311 | RNABase::Uracil, 312 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 313 | ) => Self::Phenylalanine, 314 | 315 | // Glycine 316 | (RNABase::Guanine, RNABase::Guanine, _) => Self::Glycine, 317 | 318 | // Histidine 319 | ( 320 | RNABase::Cytosine, 321 | RNABase::Adenine, 322 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 323 | ) => Self::Histidine, 324 | 325 | // Isoleucine 326 | ( 327 | RNABase::Adenine, 328 | RNABase::Uracil, 329 | RNABase::Adenine 330 | | RNABase::Cytosine 331 | | RNABase::Uracil 332 | | RNABase::AdenineCytosine 333 | | RNABase::AdenineUracil 334 | | RNABase::CytosineUracil 335 | | RNABase::AdenineCytosineUracil, 336 | ) => Self::Isoleucine, 337 | 338 | // Lysine 339 | ( 340 | RNABase::Adenine, 341 | RNABase::Adenine, 342 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 343 | ) => Self::Lysine, 344 | 345 | // Leucine 346 | (RNABase::Cytosine, RNABase::Uracil, _) 347 | | ( 348 | RNABase::Uracil, 349 | RNABase::Uracil, 350 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 351 | ) => Self::Leucine, 352 | 353 | // Methionine 354 | (RNABase::Adenine, RNABase::Uracil, RNABase::Guanine) => Self::Methionine, 355 | 356 | // Asparagine 357 | ( 358 | RNABase::Adenine, 359 | RNABase::Adenine, 360 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 361 | ) => Self::Asparagine, 362 | 363 | // Proline 364 | (RNABase::Cytosine, RNABase::Cytosine, _) => Self::Proline, 365 | 366 | // Glutamine 367 | ( 368 | RNABase::Cytosine, 369 | RNABase::Adenine, 370 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 371 | ) => Self::Glutamine, 372 | 373 | // Arginine 374 | (RNABase::Cytosine, RNABase::Guanine, _) 375 | | ( 376 | RNABase::Adenine | RNABase::AdenineCytosine, 377 | RNABase::Guanine, 378 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 379 | ) => Self::Arginine, 380 | 381 | // Serine 382 | (RNABase::Uracil, RNABase::Cytosine, _) 383 | | ( 384 | RNABase::Adenine, 385 | RNABase::Guanine, 386 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 387 | ) => Self::Serine, 388 | 389 | // Threonine 390 | (RNABase::Adenine, RNABase::Cytosine, _) => Self::Threonine, 391 | 392 | // Valine 393 | (RNABase::Guanine, RNABase::Uracil, _) => Self::Valine, 394 | 395 | // Tryptophan 396 | (RNABase::Uracil, RNABase::Guanine, RNABase::Guanine) => Self::Tryptophan, 397 | 398 | // Tyrosine 399 | ( 400 | RNABase::Uracil, 401 | RNABase::Adenine, 402 | RNABase::Cytosine | RNABase::Uracil | RNABase::CytosineUracil, 403 | ) => Self::Tyrosine, 404 | 405 | ( 406 | RNABase::CytosineGuanine, 407 | RNABase::Adenine, 408 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 409 | ) => Self::GlutamineGlutamicAcid, 410 | 411 | // Stop 412 | ( 413 | RNABase::Uracil, 414 | RNABase::Adenine, 415 | RNABase::Adenine | RNABase::Guanine | RNABase::AdenineGuanine, 416 | ) 417 | | (RNABase::Uracil, RNABase::Guanine | RNABase::AdenineGuanine, RNABase::Adenine) => { 418 | return Err(StopTranslation::new_err("stop translation")) 419 | } 420 | 421 | _ => return Err(pyo3::exceptions::PyValueError::new_err("ambiguous codon")), 422 | }) 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/aminoacidsequence.rs: -------------------------------------------------------------------------------- 1 | use crate::aminoacid::AminoAcid; 2 | use crate::impl_sequence; 3 | use crate::member::MemberOrMembers; 4 | use crate::sequence::{Sequence, SequenceInput}; 5 | use crate::utils::IntOrSlice; 6 | use pyo3::prelude::*; 7 | 8 | #[pyclass] 9 | #[derive(FromPyObject)] 10 | pub struct AminoAcidSequence { 11 | pub sequence: Vec, 12 | } 13 | 14 | #[pymethods] 15 | impl AminoAcidSequence { 16 | #[new] 17 | #[pyo3(signature = (sequence = AminoAcidSequenceInput::Sequence(SequenceInput::Seq(vec![]))))] 18 | pub fn __new__(sequence: AminoAcidSequenceInput) -> PyResult { 19 | sequence.try_into() 20 | } 21 | 22 | #[pyo3(name = "count", signature = (sequence, overlap = false))] 23 | fn py_count(&self, sequence: AminoAcidSequenceInput, overlap: bool) -> PyResult { 24 | self.count(&AminoAcidSequence::try_from(sequence)?, overlap) 25 | } 26 | 27 | #[pyo3(name = "find")] 28 | fn py_find(&self, sequence: AminoAcidSequenceInput) -> PyResult> { 29 | self.find(&AminoAcidSequence::try_from(sequence)?) 30 | } 31 | 32 | fn __repr__(&self) -> String { 33 | self.repr() 34 | } 35 | 36 | fn __str__(&self) -> String { 37 | self.str() 38 | } 39 | 40 | fn __eq__(&self, other: &Self) -> bool { 41 | self.eq(other) 42 | } 43 | 44 | fn __bool__(&self) -> bool { 45 | self.bool() 46 | } 47 | 48 | fn __add__(&self, other: AminoAcidSequenceInput) -> PyResult { 49 | Ok(self.add(&AminoAcidSequence::try_from(other)?, false).into()) 50 | } 51 | 52 | fn __radd__(&self, other: AminoAcidSequenceInput) -> PyResult { 53 | Ok(self.add(&AminoAcidSequence::try_from(other)?, true).into()) 54 | } 55 | 56 | fn __contains__(&self, sequence: AminoAcidSequenceInput) -> PyResult { 57 | self.contains(&AminoAcidSequence::try_from(sequence)?) 58 | } 59 | 60 | fn __len__(&self) -> usize { 61 | self.len() 62 | } 63 | 64 | fn __getitem__<'py>( 65 | &self, 66 | py: Python<'py>, 67 | index_or_slice: IntOrSlice, 68 | ) -> PyResult> { 69 | Ok(match self.getitem(index_or_slice)? { 70 | MemberOrMembers::Member(member) => member.into_pyobject(py)?.into_any(), 71 | MemberOrMembers::Sequence(sequence) => { 72 | Self::from(sequence).into_pyobject(py)?.into_any() 73 | } 74 | }) 75 | } 76 | } 77 | 78 | impl_sequence!(AminoAcidSequence, AminoAcid, "AminoAcidSequence"); 79 | 80 | #[derive(FromPyObject)] 81 | pub enum AminoAcidSequenceInput<'py> { 82 | AminoAcidSequence(AminoAcidSequence), 83 | Sequence(SequenceInput<'py, AminoAcid>), 84 | } 85 | 86 | impl<'py> TryFrom> for AminoAcidSequence { 87 | type Error = PyErr; 88 | 89 | fn try_from(sequence: AminoAcidSequenceInput<'py>) -> PyResult { 90 | match sequence { 91 | AminoAcidSequenceInput::AminoAcidSequence(sequence) => Ok(sequence), 92 | AminoAcidSequenceInput::Sequence(sequence) => Ok(Vec::try_from(sequence)?.into()), 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/dnabase.rs: -------------------------------------------------------------------------------- 1 | use crate::dnasequence::{DNASequence, DNASequenceInput}; 2 | use crate::member::Member; 3 | use crate::rnabase::RNABase; 4 | use pyo3::prelude::*; 5 | use std::fmt; 6 | 7 | #[pyclass(eq, eq_int, frozen, rename_all = "SCREAMING_SNAKE_CASE")] 8 | #[derive(Clone, Copy, PartialEq)] 9 | pub enum DNABase { 10 | Adenine, 11 | Cytosine, 12 | Guanine, 13 | Thymine, 14 | AdenineCytosine, 15 | AdenineGuanine, 16 | AdenineThymine, 17 | CytosineGuanine, 18 | CytosineThymine, 19 | GuanineThymine, 20 | AdenineCytosineGuanine, 21 | AdenineCytosineThymine, 22 | AdenineGuanineThymine, 23 | CytosineGuanineThymine, 24 | Any, 25 | Gap, 26 | } 27 | 28 | impl Member for DNABase {} 29 | 30 | #[pymethods] 31 | impl DNABase { 32 | #[new] 33 | pub fn __new__(code: char) -> PyResult { 34 | Self::try_from(code) 35 | } 36 | 37 | #[getter] 38 | pub fn get_code(&self) -> char { 39 | self.into() 40 | } 41 | 42 | #[getter] 43 | pub fn get_complement(&self) -> Self { 44 | match self { 45 | Self::Adenine => Self::Thymine, 46 | Self::Cytosine => Self::Guanine, 47 | Self::Guanine => Self::Cytosine, 48 | Self::Thymine => Self::Adenine, 49 | Self::AdenineCytosine => Self::GuanineThymine, 50 | Self::AdenineGuanine => Self::CytosineThymine, 51 | Self::AdenineThymine => Self::AdenineThymine, 52 | Self::CytosineGuanine => Self::CytosineGuanine, 53 | Self::CytosineThymine => Self::AdenineGuanine, 54 | Self::GuanineThymine => Self::AdenineCytosine, 55 | Self::AdenineCytosineGuanine => Self::CytosineGuanineThymine, 56 | Self::AdenineCytosineThymine => Self::AdenineGuanineThymine, 57 | Self::AdenineGuanineThymine => Self::AdenineCytosineThymine, 58 | Self::CytosineGuanineThymine => Self::AdenineCytosineGuanine, 59 | Self::Any => Self::Any, 60 | Self::Gap => Self::Gap, 61 | } 62 | } 63 | 64 | pub fn transcribe(&self) -> RNABase { 65 | self.into() 66 | } 67 | 68 | fn __bool__(&self) -> bool { 69 | *self != Self::Gap 70 | } 71 | 72 | fn __invert__(&self) -> Self { 73 | self.get_complement() 74 | } 75 | 76 | fn __add__(&self, other: DNASequenceInput) -> PyResult { 77 | Ok(self.add(&DNASequence::try_from(other)?, false).into()) 78 | } 79 | 80 | fn __radd__(&self, other: DNASequenceInput) -> PyResult { 81 | Ok(self.add(&DNASequence::try_from(other)?, true).into()) 82 | } 83 | 84 | fn __str__(&self) -> String { 85 | self.to_string() 86 | } 87 | } 88 | 89 | impl From<&DNABase> for char { 90 | fn from(base: &DNABase) -> Self { 91 | match base { 92 | DNABase::Adenine => 'A', 93 | DNABase::Cytosine => 'C', 94 | DNABase::Guanine => 'G', 95 | DNABase::Thymine => 'T', 96 | DNABase::AdenineCytosine => 'M', 97 | DNABase::AdenineGuanine => 'R', 98 | DNABase::AdenineThymine => 'W', 99 | DNABase::CytosineGuanine => 'S', 100 | DNABase::CytosineThymine => 'Y', 101 | DNABase::GuanineThymine => 'K', 102 | DNABase::AdenineCytosineGuanine => 'V', 103 | DNABase::AdenineCytosineThymine => 'H', 104 | DNABase::AdenineGuanineThymine => 'D', 105 | DNABase::CytosineGuanineThymine => 'B', 106 | DNABase::Any => 'N', 107 | DNABase::Gap => '-', 108 | } 109 | } 110 | } 111 | 112 | impl TryFrom for DNABase { 113 | type Error = PyErr; 114 | 115 | fn try_from(code: char) -> PyResult { 116 | Ok(match code { 117 | 'A' => Self::Adenine, 118 | 'C' => Self::Cytosine, 119 | 'G' => Self::Guanine, 120 | 'T' => Self::Thymine, 121 | 'M' => Self::AdenineCytosine, 122 | 'R' => Self::AdenineGuanine, 123 | 'W' => Self::AdenineThymine, 124 | 'S' => Self::CytosineGuanine, 125 | 'Y' => Self::CytosineThymine, 126 | 'K' => Self::GuanineThymine, 127 | 'V' => Self::AdenineCytosineGuanine, 128 | 'H' => Self::AdenineCytosineThymine, 129 | 'D' => Self::AdenineGuanineThymine, 130 | 'B' => Self::CytosineGuanineThymine, 131 | 'N' => Self::Any, 132 | '.' | '-' => Self::Gap, 133 | _ => { 134 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 135 | "invalid IUPAC DNA code \"{code}\"" 136 | ))) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | impl fmt::Display for DNABase { 143 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 144 | write!( 145 | f, 146 | "{}", 147 | match self { 148 | Self::Adenine => "adenine", 149 | Self::Cytosine => "cytosine", 150 | Self::Guanine => "guanine", 151 | Self::Thymine => "thymine", 152 | Self::AdenineCytosine => "adenine/cytosine", 153 | Self::AdenineGuanine => "adenine/guanine", 154 | Self::AdenineThymine => "adenine/thymine", 155 | Self::CytosineGuanine => "cytosine/guanine", 156 | Self::CytosineThymine => "cytosine/thymine", 157 | Self::GuanineThymine => "guanine/thymine", 158 | Self::AdenineCytosineGuanine => "adenine/cytosine/guanine", 159 | Self::AdenineCytosineThymine => "adenine/cytosine/thymine", 160 | Self::AdenineGuanineThymine => "adenine/guanine/thymine", 161 | Self::CytosineGuanineThymine => "cytosine/guanine/thymine", 162 | Self::Any => "any", 163 | Self::Gap => "gap", 164 | } 165 | ) 166 | } 167 | } 168 | 169 | impl From<&RNABase> for DNABase { 170 | fn from(base: &RNABase) -> Self { 171 | match base { 172 | RNABase::Adenine => Self::Adenine, 173 | RNABase::Cytosine => Self::Cytosine, 174 | RNABase::Guanine => Self::Guanine, 175 | RNABase::Uracil => Self::Thymine, 176 | RNABase::AdenineCytosine => Self::AdenineCytosine, 177 | RNABase::AdenineGuanine => Self::AdenineGuanine, 178 | RNABase::AdenineUracil => Self::AdenineThymine, 179 | RNABase::CytosineGuanine => Self::CytosineGuanine, 180 | RNABase::CytosineUracil => Self::CytosineThymine, 181 | RNABase::GuanineUracil => Self::GuanineThymine, 182 | RNABase::AdenineCytosineGuanine => Self::AdenineCytosineGuanine, 183 | RNABase::AdenineCytosineUracil => Self::AdenineCytosineThymine, 184 | RNABase::AdenineGuanineUracil => Self::AdenineGuanineThymine, 185 | RNABase::CytosineGuanineUracil => Self::CytosineGuanineThymine, 186 | RNABase::Any => Self::Any, 187 | RNABase::Gap => Self::Gap, 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/dnasequence.rs: -------------------------------------------------------------------------------- 1 | use crate::dnabase::DNABase; 2 | use crate::impl_sequence; 3 | use crate::member::MemberOrMembers; 4 | use crate::rnabase::RNABase; 5 | use crate::rnasequence::RNASequence; 6 | use crate::sequence::{Sequence, SequenceInput}; 7 | use crate::utils::IntOrSlice; 8 | use pyo3::prelude::*; 9 | use rayon::prelude::*; 10 | 11 | #[pyclass] 12 | #[derive(FromPyObject)] 13 | pub struct DNASequence { 14 | pub sequence: Vec, 15 | } 16 | 17 | #[pymethods] 18 | impl DNASequence { 19 | #[new] 20 | #[pyo3(signature = (sequence = DNASequenceInput::Sequence(SequenceInput::Seq(vec![]))))] 21 | pub fn __new__(sequence: DNASequenceInput) -> PyResult { 22 | sequence.try_into() 23 | } 24 | 25 | #[getter] 26 | fn get_complement(&self) -> Self { 27 | self.sequence 28 | .par_iter() 29 | .map(|base| base.get_complement()) 30 | .collect::>() 31 | .into() 32 | } 33 | 34 | fn transcribe(&self) -> RNASequence { 35 | self.sequence 36 | .par_iter() 37 | .map(RNABase::from) 38 | .collect::>() 39 | .into() 40 | } 41 | 42 | #[pyo3(name = "count", signature = (sequence, overlap = false))] 43 | fn py_count(&self, sequence: DNASequenceInput, overlap: bool) -> PyResult { 44 | self.count(&DNASequence::try_from(sequence)?, overlap) 45 | } 46 | 47 | #[pyo3(name = "find")] 48 | fn py_find(&self, sequence: DNASequenceInput) -> PyResult> { 49 | self.find(&DNASequence::try_from(sequence)?) 50 | } 51 | 52 | fn __invert__(&self) -> Self { 53 | self.get_complement() 54 | } 55 | 56 | fn __repr__(&self) -> String { 57 | self.repr() 58 | } 59 | 60 | fn __str__(&self) -> String { 61 | self.str() 62 | } 63 | 64 | fn __eq__(&self, other: &Self) -> bool { 65 | self.eq(other) 66 | } 67 | 68 | fn __bool__(&self) -> bool { 69 | self.bool() 70 | } 71 | 72 | fn __add__(&self, other: DNASequenceInput) -> PyResult { 73 | Ok(self.add(&DNASequence::try_from(other)?, false).into()) 74 | } 75 | 76 | fn __radd__(&self, other: DNASequenceInput) -> PyResult { 77 | Ok(self.add(&DNASequence::try_from(other)?, true).into()) 78 | } 79 | 80 | fn __len__(&self) -> usize { 81 | self.len() 82 | } 83 | 84 | fn __getitem__<'py>( 85 | &self, 86 | py: Python<'py>, 87 | index_or_slice: IntOrSlice, 88 | ) -> PyResult> { 89 | Ok(match self.getitem(index_or_slice)? { 90 | MemberOrMembers::Member(base) => base.into_pyobject(py)?.into_any(), 91 | MemberOrMembers::Sequence(sequence) => { 92 | Self::from(sequence).into_pyobject(py)?.into_any() 93 | } 94 | }) 95 | } 96 | 97 | fn __contains__(&self, sequence: DNASequenceInput) -> PyResult { 98 | self.contains(&DNASequence::try_from(sequence)?) 99 | } 100 | } 101 | 102 | impl_sequence!(DNASequence, DNABase, "DNASequence"); 103 | 104 | #[derive(FromPyObject)] 105 | pub enum DNASequenceInput<'py> { 106 | DNASequence(DNASequence), 107 | Sequence(SequenceInput<'py, DNABase>), 108 | } 109 | 110 | impl<'py> TryFrom> for DNASequence { 111 | type Error = PyErr; 112 | 113 | fn try_from(sequence: DNASequenceInput<'py>) -> PyResult { 114 | Ok(match sequence { 115 | DNASequenceInput::DNASequence(sequence) => sequence, 116 | DNASequenceInput::Sequence(sequence) => Vec::try_from(sequence)?.into(), 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod aminoacid; 2 | mod aminoacidsequence; 3 | mod dnabase; 4 | mod dnasequence; 5 | mod member; 6 | mod rnabase; 7 | mod rnasequence; 8 | #[macro_use] 9 | mod sequence; 10 | mod utils; 11 | 12 | use pyo3::prelude::*; 13 | 14 | #[pymodule] 15 | mod haem { 16 | #[pymodule_export] 17 | use crate::aminoacid::StopTranslation; 18 | 19 | #[pymodule_export] 20 | use crate::rnabase::RNABase; 21 | 22 | #[pymodule_export] 23 | use crate::rnasequence::RNASequence; 24 | 25 | #[pymodule_export] 26 | use crate::dnabase::DNABase; 27 | 28 | #[pymodule_export] 29 | use crate::dnasequence::DNASequence; 30 | 31 | #[pymodule_export] 32 | use crate::aminoacid::AminoAcid; 33 | 34 | #[pymodule_export] 35 | use crate::aminoacidsequence::AminoAcidSequence; 36 | } 37 | -------------------------------------------------------------------------------- /src/member.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use rayon::iter::{once, IntoParallelIterator}; 3 | use rayon::prelude::*; 4 | 5 | pub trait Member { 6 | fn add(&self, other: &[Self], swap: bool) -> Vec 7 | where 8 | Self: Sync + Send + Clone, 9 | for<'a> &'a [Self]: IntoParallelIterator, 10 | { 11 | match swap { 12 | true => other.par_iter().chain(once(self)).cloned().collect(), 13 | false => once(self).chain(other.par_iter()).cloned().collect(), 14 | } 15 | } 16 | } 17 | 18 | #[derive(FromPyObject)] 19 | pub enum MemberOrCode { 20 | Member(T), 21 | Code(char), 22 | } 23 | 24 | impl MemberOrCode 25 | where 26 | T: TryFrom, 27 | { 28 | pub fn into_member(self) -> PyResult { 29 | match self { 30 | MemberOrCode::Member(member) => Ok(member), 31 | MemberOrCode::Code(code) => code.try_into(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(FromPyObject)] 37 | pub enum MemberOrMembers { 38 | Member(T), 39 | Sequence(Vec), 40 | } 41 | -------------------------------------------------------------------------------- /src/rnabase.rs: -------------------------------------------------------------------------------- 1 | use crate::dnabase::DNABase; 2 | use crate::member::Member; 3 | use crate::rnasequence::{RNASequence, RNASequenceInput}; 4 | use pyo3::prelude::*; 5 | use std::fmt; 6 | 7 | #[pyclass(eq, eq_int, frozen, rename_all = "SCREAMING_SNAKE_CASE")] 8 | #[derive(Clone, Copy, PartialEq)] 9 | pub enum RNABase { 10 | Adenine, 11 | Cytosine, 12 | Guanine, 13 | Uracil, 14 | AdenineCytosine, 15 | AdenineGuanine, 16 | AdenineUracil, 17 | CytosineGuanine, 18 | CytosineUracil, 19 | GuanineUracil, 20 | AdenineCytosineGuanine, 21 | AdenineCytosineUracil, 22 | AdenineGuanineUracil, 23 | CytosineGuanineUracil, 24 | Any, 25 | Gap, 26 | } 27 | 28 | impl Member for RNABase {} 29 | 30 | #[pymethods] 31 | impl RNABase { 32 | #[new] 33 | pub fn __new__(code: char) -> PyResult { 34 | Self::try_from(code) 35 | } 36 | 37 | #[getter] 38 | fn get_code(&self) -> char { 39 | self.into() 40 | } 41 | 42 | #[getter] 43 | pub fn get_complement(&self) -> Self { 44 | match self { 45 | Self::Adenine => Self::Uracil, 46 | Self::Cytosine => Self::Guanine, 47 | Self::Guanine => Self::Cytosine, 48 | Self::Uracil => Self::Adenine, 49 | Self::AdenineCytosine => Self::GuanineUracil, 50 | Self::AdenineGuanine => Self::CytosineUracil, 51 | Self::AdenineUracil => Self::AdenineUracil, 52 | Self::CytosineGuanine => Self::CytosineGuanine, 53 | Self::CytosineUracil => Self::AdenineGuanine, 54 | Self::GuanineUracil => Self::AdenineCytosine, 55 | Self::AdenineCytosineGuanine => Self::CytosineGuanineUracil, 56 | Self::AdenineCytosineUracil => Self::AdenineGuanineUracil, 57 | Self::AdenineGuanineUracil => Self::AdenineCytosineUracil, 58 | Self::CytosineGuanineUracil => Self::AdenineCytosineGuanine, 59 | Self::Any => Self::Any, 60 | Self::Gap => Self::Gap, 61 | } 62 | } 63 | 64 | fn retro_transcribe(&self) -> DNABase { 65 | self.into() 66 | } 67 | 68 | fn __bool__(&self) -> bool { 69 | *self != Self::Gap 70 | } 71 | 72 | fn __invert__(&self) -> Self { 73 | self.get_complement() 74 | } 75 | 76 | fn __add__(&self, other: RNASequenceInput) -> PyResult { 77 | Ok(self.add(&RNASequence::try_from(other)?, false).into()) 78 | } 79 | 80 | fn __radd__(&self, other: RNASequenceInput) -> PyResult { 81 | Ok(self.add(&RNASequence::try_from(other)?, true).into()) 82 | } 83 | 84 | fn __str__(&self) -> String { 85 | self.to_string() 86 | } 87 | } 88 | 89 | impl From<&RNABase> for char { 90 | fn from(base: &RNABase) -> Self { 91 | match base { 92 | RNABase::Adenine => 'A', 93 | RNABase::Cytosine => 'C', 94 | RNABase::Guanine => 'G', 95 | RNABase::Uracil => 'U', 96 | RNABase::AdenineCytosine => 'M', 97 | RNABase::AdenineGuanine => 'R', 98 | RNABase::AdenineUracil => 'W', 99 | RNABase::CytosineGuanine => 'S', 100 | RNABase::CytosineUracil => 'Y', 101 | RNABase::GuanineUracil => 'K', 102 | RNABase::AdenineCytosineGuanine => 'V', 103 | RNABase::AdenineCytosineUracil => 'H', 104 | RNABase::AdenineGuanineUracil => 'D', 105 | RNABase::CytosineGuanineUracil => 'B', 106 | RNABase::Any => 'N', 107 | RNABase::Gap => '-', 108 | } 109 | } 110 | } 111 | 112 | impl TryFrom for RNABase { 113 | type Error = PyErr; 114 | 115 | fn try_from(code: char) -> PyResult { 116 | Ok(match code { 117 | 'A' => RNABase::Adenine, 118 | 'C' => RNABase::Cytosine, 119 | 'G' => RNABase::Guanine, 120 | 'U' => RNABase::Uracil, 121 | 'M' => RNABase::AdenineCytosine, 122 | 'R' => RNABase::AdenineGuanine, 123 | 'W' => RNABase::AdenineUracil, 124 | 'S' => RNABase::CytosineGuanine, 125 | 'Y' => RNABase::CytosineUracil, 126 | 'K' => RNABase::GuanineUracil, 127 | 'V' => RNABase::AdenineCytosineGuanine, 128 | 'H' => RNABase::AdenineCytosineUracil, 129 | 'D' => RNABase::AdenineGuanineUracil, 130 | 'B' => RNABase::CytosineGuanineUracil, 131 | 'N' => RNABase::Any, 132 | '.' | '-' => RNABase::Gap, 133 | _ => { 134 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 135 | "invalid IUPAC RNA code \"{code}\"" 136 | ))) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | impl fmt::Display for RNABase { 143 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 144 | write!( 145 | f, 146 | "{}", 147 | match self { 148 | Self::Adenine => "adenine", 149 | Self::Cytosine => "cytosine", 150 | Self::Guanine => "guanine", 151 | Self::Uracil => "uracil", 152 | Self::AdenineCytosine => "adenine/cytosine", 153 | Self::AdenineGuanine => "adenine/guanine", 154 | Self::AdenineUracil => "adenine/uracil", 155 | Self::CytosineGuanine => "cytosine/guanine", 156 | Self::CytosineUracil => "cytosine/uracil", 157 | Self::GuanineUracil => "guanine/uracil", 158 | Self::AdenineCytosineGuanine => "adenine/cytosine/guanine", 159 | Self::AdenineCytosineUracil => "adenine/cytosine/uracil", 160 | Self::AdenineGuanineUracil => "adenine/guanine/uracil", 161 | Self::CytosineGuanineUracil => "cytosine/guanine/uracil", 162 | Self::Any => "any", 163 | Self::Gap => "gap", 164 | } 165 | ) 166 | } 167 | } 168 | 169 | impl From<&DNABase> for RNABase { 170 | fn from(base: &DNABase) -> Self { 171 | match base { 172 | DNABase::Adenine => Self::Adenine, 173 | DNABase::Cytosine => Self::Cytosine, 174 | DNABase::Guanine => Self::Guanine, 175 | DNABase::Thymine => Self::Uracil, 176 | DNABase::AdenineCytosine => Self::AdenineCytosine, 177 | DNABase::AdenineGuanine => Self::AdenineGuanine, 178 | DNABase::AdenineThymine => Self::AdenineUracil, 179 | DNABase::CytosineGuanine => Self::CytosineGuanine, 180 | DNABase::CytosineThymine => Self::CytosineUracil, 181 | DNABase::GuanineThymine => Self::GuanineUracil, 182 | DNABase::AdenineCytosineGuanine => Self::AdenineCytosineGuanine, 183 | DNABase::AdenineCytosineThymine => Self::AdenineCytosineUracil, 184 | DNABase::AdenineGuanineThymine => Self::AdenineGuanineUracil, 185 | DNABase::CytosineGuanineThymine => Self::CytosineGuanineUracil, 186 | DNABase::Any => Self::Any, 187 | DNABase::Gap => Self::Gap, 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/rnasequence.rs: -------------------------------------------------------------------------------- 1 | use crate::aminoacid::{AminoAcid, StopTranslation}; 2 | use crate::aminoacidsequence::AminoAcidSequence; 3 | use crate::dnabase::DNABase; 4 | use crate::dnasequence::DNASequence; 5 | use crate::impl_sequence; 6 | use crate::member::MemberOrMembers; 7 | use crate::rnabase::RNABase; 8 | use crate::sequence::{Sequence, SequenceInput}; 9 | use crate::utils::IntOrSlice; 10 | use pyo3::exceptions::PyValueError; 11 | use pyo3::prelude::*; 12 | use rayon::prelude::*; 13 | 14 | #[pyclass] 15 | #[derive(FromPyObject)] 16 | pub struct RNASequence { 17 | pub sequence: Vec, 18 | } 19 | 20 | #[pymethods] 21 | impl RNASequence { 22 | #[new] 23 | #[pyo3(signature = (sequence = RNASequenceInput::Sequence(SequenceInput::Seq(vec![]))))] 24 | pub fn __new__(sequence: RNASequenceInput) -> PyResult { 25 | sequence.try_into() 26 | } 27 | 28 | #[getter] 29 | fn get_complement(&self) -> Self { 30 | self.sequence 31 | .par_iter() 32 | .map(|b| b.get_complement()) 33 | .collect::>() 34 | .into() 35 | } 36 | 37 | fn retro_transcribe(&self) -> DNASequence { 38 | self.sequence 39 | .par_iter() 40 | .map(DNABase::from) 41 | .collect::>() 42 | .into() 43 | } 44 | 45 | #[pyo3(name = "count", signature = (sequence, overlap = false))] 46 | fn py_count(&self, sequence: RNASequenceInput, overlap: bool) -> PyResult { 47 | self.count(&RNASequence::try_from(sequence)?, overlap) 48 | } 49 | 50 | #[pyo3(name = "find")] 51 | fn py_find(&self, sequence: RNASequenceInput) -> PyResult> { 52 | self.find(&RNASequence::try_from(sequence)?) 53 | } 54 | 55 | fn translate(&self, py: Python<'_>) -> PyResult { 56 | // Find start codon 57 | let start = self 58 | .members() 59 | .par_windows(3) 60 | .map(|codon| AminoAcid::try_from((&codon[0], &codon[1], &codon[2]))) 61 | .position_first(|member| { 62 | member.is_ok() && member.as_ref().unwrap() == &AminoAcid::Methionine 63 | }); 64 | 65 | if start.is_none() { 66 | return Err(PyValueError::new_err("no start codon found")); 67 | } 68 | 69 | // Find stop codon 70 | let stop = self.members()[start.unwrap()..self.members().len()] 71 | .chunks_exact(3) 72 | .map(|codon| AminoAcid::try_from((&codon[0], &codon[1], &codon[2]))) 73 | .position(|member| match member { 74 | Ok(_) => false, 75 | Err(err) => err.is_instance_of::(py), 76 | }); 77 | 78 | match stop.is_none() { 79 | false => Ok( 80 | self.members()[start.unwrap()..(start.unwrap() + stop.unwrap() * 3)] 81 | .par_chunks_exact(3) 82 | .map(|codon| AminoAcid::try_from((&codon[0], &codon[1], &codon[2]))) 83 | .collect::, _>>()? 84 | .into(), 85 | ), 86 | true => Err(PyValueError::new_err("no stop codon found")), 87 | } 88 | } 89 | 90 | fn __invert__(&self) -> Self { 91 | self.get_complement() 92 | } 93 | 94 | fn __repr__(&self) -> String { 95 | self.repr() 96 | } 97 | 98 | fn __str__(&self) -> String { 99 | self.str() 100 | } 101 | 102 | fn __eq__(&self, other: &Self) -> bool { 103 | self.eq(other) 104 | } 105 | 106 | fn __bool__(&self) -> bool { 107 | self.bool() 108 | } 109 | 110 | fn __add__(&self, other: RNASequenceInput) -> PyResult { 111 | Ok(self.add(&RNASequence::try_from(other)?, false).into()) 112 | } 113 | 114 | fn __radd__(&self, other: RNASequenceInput) -> PyResult { 115 | Ok(self.add(&RNASequence::try_from(other)?, true).into()) 116 | } 117 | 118 | fn __len__(&self) -> usize { 119 | self.len() 120 | } 121 | 122 | fn __getitem__<'py>( 123 | &self, 124 | py: Python<'py>, 125 | index_or_slice: IntOrSlice, 126 | ) -> PyResult> { 127 | Ok(match self.getitem(index_or_slice)? { 128 | MemberOrMembers::Member(member) => member.into_pyobject(py)?.into_any(), 129 | MemberOrMembers::Sequence(sequence) => { 130 | Self::from(sequence).into_pyobject(py)?.into_any() 131 | } 132 | }) 133 | } 134 | 135 | fn __contains__(&self, sequence: RNASequenceInput) -> PyResult { 136 | self.contains(&RNASequence::try_from(sequence)?) 137 | } 138 | } 139 | 140 | impl_sequence!(RNASequence, RNABase, "RNASequence"); 141 | 142 | #[derive(FromPyObject)] 143 | pub enum RNASequenceInput<'py> { 144 | RNASequence(RNASequence), 145 | Sequence(SequenceInput<'py, RNABase>), 146 | } 147 | 148 | impl<'py> TryFrom> for RNASequence { 149 | type Error = PyErr; 150 | 151 | fn try_from(sequence: RNASequenceInput<'py>) -> PyResult { 152 | Ok(match sequence { 153 | RNASequenceInput::RNASequence(sequence) => sequence, 154 | RNASequenceInput::Sequence(sequence) => Vec::try_from(sequence)?.into(), 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/sequence.rs: -------------------------------------------------------------------------------- 1 | use crate::member::{MemberOrCode, MemberOrMembers}; 2 | use crate::utils::IntOrSlice; 3 | use pyo3::prelude::*; 4 | use pyo3::pybacked::PyBackedStr; 5 | use pyo3::pyclass::PyClass; 6 | use pyo3::types::PyIterator; 7 | use rayon::prelude::*; 8 | 9 | pub trait Sequence 10 | where 11 | T: PartialEq + Clone + Sync, 12 | for<'a> char: From<&'a T>, 13 | { 14 | fn members(&self) -> &Vec; 15 | fn name(&self) -> &str; 16 | 17 | fn bool(&self) -> bool { 18 | !self.members().is_empty() 19 | } 20 | 21 | fn eq(&self, other: &Self) -> bool { 22 | self.members() == other.members() 23 | } 24 | 25 | fn repr(&self) -> String { 26 | match self.members().is_empty() { 27 | true => format!("<{}>", self.name()), 28 | false => format!( 29 | "<{}: {}>", 30 | self.name(), 31 | self.members() 32 | .par_iter() 33 | .map(char::from) 34 | .collect::(), 35 | ), 36 | } 37 | } 38 | 39 | fn str(&self) -> String { 40 | match self.len() { 41 | length if length < 21 => self.members().iter().map(char::from).collect::<_>(), 42 | _ => format!( 43 | "{}...{}", 44 | self.members()[0..10] 45 | .iter() 46 | .map(char::from) 47 | .collect::(), 48 | self.members()[self.len() - 10..self.len()] 49 | .iter() 50 | .map(char::from) 51 | .collect::() 52 | ), 53 | } 54 | } 55 | 56 | fn contains(&self, sequence: &[T]) -> PyResult { 57 | Ok(match sequence.is_empty() { 58 | true => true, 59 | false => self 60 | .members() 61 | .par_windows(sequence.len()) 62 | .any(|w| w == sequence), 63 | }) 64 | } 65 | 66 | fn add(&self, other: &[T], swap: bool) -> Vec 67 | where 68 | T: Send, 69 | for<'a> &'a [T]: IntoParallelIterator, 70 | { 71 | match swap { 72 | true => other 73 | .par_iter() 74 | .chain(self.members().par_iter()) 75 | .cloned() 76 | .collect(), 77 | false => self 78 | .members() 79 | .par_iter() 80 | .chain(other.par_iter()) 81 | .cloned() 82 | .collect(), 83 | } 84 | } 85 | 86 | fn getitem(&self, index_or_slice: IntOrSlice) -> PyResult> { 87 | match index_or_slice { 88 | IntOrSlice::Int(index) => { 89 | let index = match index { 90 | index if index < 0 => self.len() - index.unsigned_abs(), 91 | _ => index as usize, 92 | }; 93 | 94 | match index { 95 | index if index < self.len() => { 96 | Ok(MemberOrMembers::Member(self.members()[index].clone())) 97 | } 98 | _ => Err(pyo3::exceptions::PyIndexError::new_err(format!( 99 | "{} index out of range", 100 | self.name() 101 | ))), 102 | } 103 | } 104 | IntOrSlice::Slice(slice) => { 105 | let indices = slice.indices(self.len() as isize)?; 106 | 107 | Ok(MemberOrMembers::Sequence(match indices.step { 108 | s if s < 0 => (indices.stop + 1..indices.start + 1) 109 | .rev() 110 | .step_by(indices.step.unsigned_abs()) 111 | .map(|i| self.members()[i as usize].clone()) 112 | .collect(), 113 | _ => (indices.start..indices.stop) 114 | .step_by(indices.step as usize) 115 | .map(|i| self.members()[i as usize].clone()) 116 | .collect(), 117 | })) 118 | } 119 | } 120 | } 121 | 122 | fn len(&self) -> usize { 123 | self.members().len() 124 | } 125 | 126 | fn count(&self, sequence: &[T], overlap: bool) -> PyResult { 127 | Ok(match (sequence.len(), overlap) { 128 | // Special case, empty sequences always return 0. 129 | (0, _) => 0, 130 | // With a sequence lenth of 1 or when overlap is allowed, optimisation is possible. 131 | (len @ 1, _) | (len, true) => self 132 | .members() 133 | .par_windows(len) 134 | .filter(|w| *w == sequence) 135 | .count(), 136 | (len, _) => { 137 | let mut count = 0; 138 | 139 | let mut iter = self.members().windows(len); 140 | while let Some(item) = iter.next() { 141 | if item == sequence { 142 | count += 1; 143 | iter.nth(sequence.len() - 2); 144 | } 145 | } 146 | 147 | count 148 | } 149 | }) 150 | } 151 | 152 | fn find(&self, sequence: &[T]) -> PyResult> { 153 | Ok(if self.members().is_empty() || sequence.is_empty() { 154 | None 155 | } else { 156 | self.members() 157 | .par_windows(sequence.len()) 158 | .position_first(|w| w == sequence) 159 | }) 160 | } 161 | } 162 | 163 | #[macro_export] 164 | macro_rules! impl_sequence { 165 | ($struct_name:ident, $member_type:ty, $name:expr) => { 166 | impl $crate::sequence::Sequence<$member_type> for $struct_name { 167 | #[inline] 168 | fn members(&self) -> &Vec<$member_type> { 169 | &self.sequence 170 | } 171 | 172 | #[inline] 173 | fn name(&self) -> &str { 174 | $name 175 | } 176 | } 177 | 178 | impl From> for $struct_name { 179 | fn from(sequence: Vec<$member_type>) -> Self { 180 | Self { sequence } 181 | } 182 | } 183 | 184 | impl std::ops::Deref for $struct_name { 185 | type Target = [$member_type]; 186 | 187 | fn deref(&self) -> &Self::Target { 188 | &self.sequence 189 | } 190 | } 191 | }; 192 | } 193 | 194 | #[derive(FromPyObject)] 195 | pub enum SequenceInput<'py, T> { 196 | Str(PyBackedStr), 197 | Iter(Bound<'py, PyIterator>), 198 | Seq(Vec), 199 | SeqStr(Vec), 200 | Member(T), 201 | } 202 | 203 | impl<'a, T> TryFrom> for Vec 204 | where 205 | T: TryFrom + Send + PyClass + Clone, 206 | { 207 | type Error = PyErr; 208 | 209 | fn try_from(bases: SequenceInput<'a, T>) -> PyResult { 210 | Ok(match bases { 211 | SequenceInput::Str(bases) => bases 212 | .as_parallel_string() 213 | .par_chars() 214 | .map(T::try_from) 215 | .collect::>()?, 216 | SequenceInput::Iter(bases) => bases 217 | .into_iter() 218 | .map(|member_or_code| MemberOrCode::extract_bound(&member_or_code?)?.into_member()) 219 | .collect::>()?, 220 | SequenceInput::Seq(bases) => bases, 221 | SequenceInput::SeqStr(codes) => codes 222 | .into_par_iter() 223 | .map(T::try_from) 224 | .collect::>()?, 225 | SequenceInput::Member(base) => vec![base], 226 | }) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PySlice; 3 | 4 | #[derive(FromPyObject)] 5 | pub enum IntOrSlice<'py> { 6 | Int(isize), 7 | Slice(Bound<'py, PySlice>), 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_amino_acid.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import typing 3 | 4 | import pytest 5 | 6 | import haem 7 | 8 | 9 | @pytest.fixture 10 | def rna_bases() -> typing.Iterator[haem.RNABase]: 11 | return map(haem.RNABase, "ACGUMRWSYKVHDBN") 12 | 13 | 14 | def test__new__alanine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 15 | assert haem.AminoAcid("A") == haem.AminoAcid.ALANINE 16 | 17 | for base in rna_bases: 18 | assert ( 19 | haem.AminoAcid((haem.RNABase.GUANINE, haem.RNABase.CYTOSINE, base)) 20 | == haem.AminoAcid.ALANINE 21 | ) 22 | 23 | assert haem.AminoAcid("GC" + base.code) == haem.AminoAcid.ALANINE 24 | 25 | 26 | def test__new__aspartic_acid_asparagine() -> None: 27 | assert haem.AminoAcid("B") == haem.AminoAcid.ASPARTIC_ACID_ASPARAGINE 28 | 29 | codons = [ 30 | (haem.RNABase.ADENINE_GUANINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE), 31 | (haem.RNABase.ADENINE_GUANINE, haem.RNABase.ADENINE, haem.RNABase.URACIL), 32 | ( 33 | haem.RNABase.ADENINE_GUANINE, 34 | haem.RNABase.ADENINE, 35 | haem.RNABase.CYTOSINE_URACIL, 36 | ), 37 | ] 38 | 39 | for codon in codons: 40 | assert haem.AminoAcid(codon) == haem.AminoAcid.ASPARTIC_ACID_ASPARAGINE 41 | 42 | assert ( 43 | haem.AminoAcid("".join(base.code for base in codon)) 44 | == haem.AminoAcid.ASPARTIC_ACID_ASPARAGINE 45 | ) 46 | 47 | 48 | def test__new__cysteine() -> None: 49 | assert haem.AminoAcid("C") == haem.AminoAcid.CYSTEINE 50 | 51 | codons = [ 52 | (haem.RNABase.URACIL, haem.RNABase.GUANINE, haem.RNABase.CYTOSINE), 53 | (haem.RNABase.URACIL, haem.RNABase.GUANINE, haem.RNABase.URACIL), 54 | (haem.RNABase.URACIL, haem.RNABase.GUANINE, haem.RNABase.CYTOSINE_URACIL), 55 | ] 56 | 57 | for codon in codons: 58 | assert haem.AminoAcid(codon) == haem.AminoAcid.CYSTEINE 59 | 60 | assert ( 61 | haem.AminoAcid("".join(base.code for base in codon)) 62 | == haem.AminoAcid.CYSTEINE 63 | ) 64 | 65 | 66 | def test__new__aspartic_acid() -> None: 67 | assert haem.AminoAcid("D") == haem.AminoAcid.ASPARTIC_ACID 68 | 69 | codons = [ 70 | (haem.RNABase.GUANINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE), 71 | (haem.RNABase.GUANINE, haem.RNABase.ADENINE, haem.RNABase.URACIL), 72 | (haem.RNABase.GUANINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE_URACIL), 73 | ] 74 | 75 | for codon in codons: 76 | assert haem.AminoAcid(codon) == haem.AminoAcid.ASPARTIC_ACID 77 | 78 | assert ( 79 | haem.AminoAcid("".join(base.code for base in codon)) 80 | == haem.AminoAcid.ASPARTIC_ACID 81 | ) 82 | 83 | 84 | def test__new__glutamic_acid() -> None: 85 | assert haem.AminoAcid("E") == haem.AminoAcid.GLUTAMIC_ACID 86 | 87 | codons = [ 88 | (haem.RNABase.GUANINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE), 89 | (haem.RNABase.GUANINE, haem.RNABase.ADENINE, haem.RNABase.GUANINE), 90 | (haem.RNABase.GUANINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE_GUANINE), 91 | ] 92 | 93 | for codon in codons: 94 | assert haem.AminoAcid(codon) == haem.AminoAcid.GLUTAMIC_ACID 95 | 96 | assert ( 97 | haem.AminoAcid("".join(base.code for base in codon)) 98 | == haem.AminoAcid.GLUTAMIC_ACID 99 | ) 100 | 101 | 102 | def test__new__phenylalanine() -> None: 103 | assert haem.AminoAcid("F") == haem.AminoAcid.PHENYLALANINE 104 | 105 | codons = [ 106 | (haem.RNABase.URACIL, haem.RNABase.URACIL, haem.RNABase.CYTOSINE), 107 | (haem.RNABase.URACIL, haem.RNABase.URACIL, haem.RNABase.URACIL), 108 | (haem.RNABase.URACIL, haem.RNABase.URACIL, haem.RNABase.CYTOSINE_URACIL), 109 | ] 110 | 111 | for codon in codons: 112 | assert haem.AminoAcid(codon) == haem.AminoAcid.PHENYLALANINE 113 | 114 | assert ( 115 | haem.AminoAcid("".join(base.code for base in codon)) 116 | == haem.AminoAcid.PHENYLALANINE 117 | ) 118 | 119 | 120 | def test__new__glycine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 121 | assert haem.AminoAcid("G") == haem.AminoAcid.GLYCINE 122 | 123 | for base in rna_bases: 124 | assert ( 125 | haem.AminoAcid((haem.RNABase.GUANINE, haem.RNABase.GUANINE, base)) 126 | == haem.AminoAcid.GLYCINE 127 | ) 128 | 129 | assert haem.AminoAcid("GG" + base.code) == haem.AminoAcid.GLYCINE 130 | 131 | 132 | def test__new__histidine() -> None: 133 | assert haem.AminoAcid("H") == haem.AminoAcid.HISTIDINE 134 | 135 | codons = [ 136 | (haem.RNABase.CYTOSINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE), 137 | (haem.RNABase.CYTOSINE, haem.RNABase.ADENINE, haem.RNABase.URACIL), 138 | (haem.RNABase.CYTOSINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE_URACIL), 139 | ] 140 | 141 | for codon in codons: 142 | assert haem.AminoAcid(codon) == haem.AminoAcid.HISTIDINE 143 | 144 | assert ( 145 | haem.AminoAcid("".join(base.code for base in codon)) 146 | == haem.AminoAcid.HISTIDINE 147 | ) 148 | 149 | 150 | def test__new__iso_leucine() -> None: 151 | assert haem.AminoAcid("I") == haem.AminoAcid.ISOLEUCINE 152 | 153 | codons = [ 154 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.ADENINE), 155 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.CYTOSINE), 156 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.URACIL), 157 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.ADENINE_CYTOSINE), 158 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.ADENINE_URACIL), 159 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.CYTOSINE_URACIL), 160 | ( 161 | haem.RNABase.ADENINE, 162 | haem.RNABase.URACIL, 163 | haem.RNABase.ADENINE_CYTOSINE_URACIL, 164 | ), 165 | ] 166 | 167 | for codon in codons: 168 | assert haem.AminoAcid(codon) == haem.AminoAcid.ISOLEUCINE 169 | 170 | assert ( 171 | haem.AminoAcid("".join(base.code for base in codon)) 172 | == haem.AminoAcid.ISOLEUCINE 173 | ) 174 | 175 | 176 | def test__new__lysine() -> None: 177 | assert haem.AminoAcid("K") == haem.AminoAcid.LYSINE 178 | 179 | codons = [ 180 | (haem.RNABase.ADENINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE), 181 | (haem.RNABase.ADENINE, haem.RNABase.ADENINE, haem.RNABase.GUANINE), 182 | (haem.RNABase.ADENINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE_GUANINE), 183 | ] 184 | 185 | for codon in codons: 186 | assert haem.AminoAcid(codon) == haem.AminoAcid.LYSINE 187 | 188 | assert ( 189 | haem.AminoAcid("".join(base.code for base in codon)) 190 | == haem.AminoAcid.LYSINE 191 | ) 192 | 193 | 194 | def test__new__leucine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 195 | assert haem.AminoAcid("L") == haem.AminoAcid.LEUCINE 196 | 197 | for base in rna_bases: 198 | assert ( 199 | haem.AminoAcid((haem.RNABase.CYTOSINE, haem.RNABase.URACIL, base)) 200 | == haem.AminoAcid.LEUCINE 201 | ) 202 | 203 | assert haem.AminoAcid("CU" + base.code) == haem.AminoAcid.LEUCINE 204 | 205 | codons = [ 206 | (haem.RNABase.URACIL, haem.RNABase.URACIL, haem.RNABase.ADENINE), 207 | (haem.RNABase.URACIL, haem.RNABase.URACIL, haem.RNABase.GUANINE), 208 | (haem.RNABase.URACIL, haem.RNABase.URACIL, haem.RNABase.ADENINE_GUANINE), 209 | ] 210 | 211 | for codon in codons: 212 | assert haem.AminoAcid(codon) == haem.AminoAcid.LEUCINE 213 | 214 | assert ( 215 | haem.AminoAcid("".join(base.code for base in codon)) 216 | == haem.AminoAcid.LEUCINE 217 | ) 218 | 219 | 220 | def test__new__methionine() -> None: 221 | assert haem.AminoAcid("M") == haem.AminoAcid.METHIONINE 222 | assert ( 223 | haem.AminoAcid( 224 | (haem.RNABase.ADENINE, haem.RNABase.URACIL, haem.RNABase.GUANINE) 225 | ) 226 | == haem.AminoAcid.METHIONINE 227 | ) 228 | assert haem.AminoAcid("AUG") == haem.AminoAcid.METHIONINE 229 | 230 | 231 | def test__new__asparagine() -> None: 232 | assert haem.AminoAcid("N") == haem.AminoAcid.ASPARAGINE 233 | 234 | codons = [ 235 | (haem.RNABase.ADENINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE), 236 | (haem.RNABase.ADENINE, haem.RNABase.ADENINE, haem.RNABase.URACIL), 237 | (haem.RNABase.ADENINE, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE_URACIL), 238 | ] 239 | 240 | for codon in codons: 241 | assert haem.AminoAcid(codon) == haem.AminoAcid.ASPARAGINE 242 | 243 | assert ( 244 | haem.AminoAcid("".join(base.code for base in codon)) 245 | == haem.AminoAcid.ASPARAGINE 246 | ) 247 | 248 | 249 | def test__new__proline(rna_bases: typing.Iterator[haem.RNABase]) -> None: 250 | assert haem.AminoAcid("P") == haem.AminoAcid.PROLINE 251 | 252 | for base in rna_bases: 253 | assert ( 254 | haem.AminoAcid((haem.RNABase.CYTOSINE, haem.RNABase.CYTOSINE, base)) 255 | == haem.AminoAcid.PROLINE 256 | ) 257 | 258 | assert haem.AminoAcid("CC" + base.code) == haem.AminoAcid.PROLINE 259 | 260 | 261 | def test__new__glutamine() -> None: 262 | assert haem.AminoAcid("Q") == haem.AminoAcid.GLUTAMINE 263 | 264 | codons = [ 265 | (haem.RNABase.CYTOSINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE), 266 | (haem.RNABase.CYTOSINE, haem.RNABase.ADENINE, haem.RNABase.GUANINE), 267 | (haem.RNABase.CYTOSINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE_GUANINE), 268 | ] 269 | 270 | for codon in codons: 271 | assert haem.AminoAcid(codon) == haem.AminoAcid.GLUTAMINE 272 | 273 | assert ( 274 | haem.AminoAcid("".join(base.code for base in codon)) 275 | == haem.AminoAcid.GLUTAMINE 276 | ) 277 | 278 | 279 | def test__new__arginine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 280 | assert haem.AminoAcid("R") == haem.AminoAcid.ARGININE 281 | 282 | for base in rna_bases: 283 | assert ( 284 | haem.AminoAcid((haem.RNABase.CYTOSINE, haem.RNABase.GUANINE, base)) 285 | == haem.AminoAcid.ARGININE 286 | ) 287 | 288 | assert haem.AminoAcid("CG" + base.code) == haem.AminoAcid.ARGININE 289 | 290 | codons = [ 291 | (haem.RNABase.ADENINE, haem.RNABase.GUANINE, haem.RNABase.ADENINE), 292 | (haem.RNABase.ADENINE, haem.RNABase.GUANINE, haem.RNABase.GUANINE), 293 | (haem.RNABase.ADENINE, haem.RNABase.GUANINE, haem.RNABase.ADENINE_GUANINE), 294 | (haem.RNABase.ADENINE_CYTOSINE, haem.RNABase.GUANINE, haem.RNABase.ADENINE), 295 | (haem.RNABase.ADENINE_CYTOSINE, haem.RNABase.GUANINE, haem.RNABase.GUANINE), 296 | ( 297 | haem.RNABase.ADENINE_CYTOSINE, 298 | haem.RNABase.GUANINE, 299 | haem.RNABase.ADENINE_GUANINE, 300 | ), 301 | ] 302 | 303 | for codon in codons: 304 | assert haem.AminoAcid(codon) == haem.AminoAcid.ARGININE 305 | 306 | assert ( 307 | haem.AminoAcid("".join(base.code for base in codon)) 308 | == haem.AminoAcid.ARGININE 309 | ) 310 | 311 | 312 | def test__new__serine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 313 | assert haem.AminoAcid("S") == haem.AminoAcid.SERINE 314 | 315 | for base in rna_bases: 316 | assert ( 317 | haem.AminoAcid((haem.RNABase.URACIL, haem.RNABase.CYTOSINE, base)) 318 | == haem.AminoAcid.SERINE 319 | ) 320 | 321 | assert haem.AminoAcid("UC" + base.code) == haem.AminoAcid.SERINE 322 | 323 | codons = [ 324 | (haem.RNABase.ADENINE, haem.RNABase.GUANINE, haem.RNABase.CYTOSINE), 325 | (haem.RNABase.ADENINE, haem.RNABase.GUANINE, haem.RNABase.URACIL), 326 | (haem.RNABase.ADENINE, haem.RNABase.GUANINE, haem.RNABase.CYTOSINE_URACIL), 327 | ] 328 | 329 | for codon in codons: 330 | assert haem.AminoAcid(codon) == haem.AminoAcid.SERINE 331 | 332 | assert ( 333 | haem.AminoAcid("".join(base.code for base in codon)) 334 | == haem.AminoAcid.SERINE 335 | ) 336 | 337 | 338 | def test__new__threonine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 339 | assert haem.AminoAcid("T") == haem.AminoAcid.THREONINE 340 | 341 | for base in rna_bases: 342 | assert ( 343 | haem.AminoAcid((haem.RNABase.ADENINE, haem.RNABase.CYTOSINE, base)) 344 | == haem.AminoAcid.THREONINE 345 | ) 346 | 347 | assert haem.AminoAcid("AC" + base.code) == haem.AminoAcid.THREONINE 348 | 349 | 350 | def test__new__valine(rna_bases: typing.Iterator[haem.RNABase]) -> None: 351 | assert haem.AminoAcid("V") == haem.AminoAcid.VALINE 352 | 353 | for base in rna_bases: 354 | assert ( 355 | haem.AminoAcid((haem.RNABase.GUANINE, haem.RNABase.URACIL, base)) 356 | == haem.AminoAcid.VALINE 357 | ) 358 | 359 | assert haem.AminoAcid("GU" + base.code) == haem.AminoAcid.VALINE 360 | 361 | 362 | def test__new__tryptophan(rna_bases: typing.Iterator[haem.RNABase]) -> None: 363 | assert haem.AminoAcid("W") == haem.AminoAcid.TRYPTOPHAN 364 | assert ( 365 | haem.AminoAcid( 366 | (haem.RNABase.URACIL, haem.RNABase.GUANINE, haem.RNABase.GUANINE) 367 | ) 368 | == haem.AminoAcid.TRYPTOPHAN 369 | ) 370 | assert haem.AminoAcid("UGG") == haem.AminoAcid.TRYPTOPHAN 371 | 372 | 373 | def test__new__any() -> None: 374 | assert haem.AminoAcid("X") == haem.AminoAcid.ANY 375 | 376 | 377 | def test__new__tyrosine() -> None: 378 | assert haem.AminoAcid("Y") == haem.AminoAcid.TYROSINE 379 | 380 | codons = [ 381 | (haem.RNABase.URACIL, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE), 382 | (haem.RNABase.URACIL, haem.RNABase.ADENINE, haem.RNABase.URACIL), 383 | (haem.RNABase.URACIL, haem.RNABase.ADENINE, haem.RNABase.CYTOSINE_URACIL), 384 | ] 385 | 386 | for codon in codons: 387 | assert haem.AminoAcid(codon) == haem.AminoAcid.TYROSINE 388 | 389 | assert ( 390 | haem.AminoAcid("".join(base.code for base in codon)) 391 | == haem.AminoAcid.TYROSINE 392 | ) 393 | 394 | 395 | def test__new__glutamine_glutamic_acid() -> None: 396 | assert haem.AminoAcid("Z") == haem.AminoAcid.GLUTAMINE_GLUTAMIC_ACID 397 | 398 | codons = [ 399 | (haem.RNABase.CYTOSINE_GUANINE, haem.RNABase.ADENINE, haem.RNABase.ADENINE), 400 | (haem.RNABase.CYTOSINE_GUANINE, haem.RNABase.ADENINE, haem.RNABase.GUANINE), 401 | ( 402 | haem.RNABase.CYTOSINE_GUANINE, 403 | haem.RNABase.ADENINE, 404 | haem.RNABase.ADENINE_GUANINE, 405 | ), 406 | ] 407 | 408 | for codon in codons: 409 | assert haem.AminoAcid(codon) == haem.AminoAcid.GLUTAMINE_GLUTAMIC_ACID 410 | 411 | assert ( 412 | haem.AminoAcid("".join(base.code for base in codon)) 413 | == haem.AminoAcid.GLUTAMINE_GLUTAMIC_ACID 414 | ) 415 | 416 | 417 | def test__new__sequence_str() -> None: 418 | assert haem.AminoAcid(("U", "A", "C")) == haem.AminoAcid.TYROSINE 419 | 420 | 421 | def test__new__stop() -> None: 422 | codons = [ 423 | (haem.RNABase.URACIL, haem.RNABase.ADENINE, haem.RNABase.ADENINE), 424 | (haem.RNABase.URACIL, haem.RNABase.ADENINE, haem.RNABase.GUANINE), 425 | (haem.RNABase.URACIL, haem.RNABase.ADENINE, haem.RNABase.ADENINE_GUANINE), 426 | (haem.RNABase.URACIL, haem.RNABase.GUANINE, haem.RNABase.ADENINE), 427 | (haem.RNABase.URACIL, haem.RNABase.ADENINE_GUANINE, haem.RNABase.ADENINE), 428 | ] 429 | 430 | for codon in codons: 431 | with pytest.raises(haem.StopTranslation) as excinfo: 432 | haem.AminoAcid(codon) 433 | 434 | assert str(excinfo.value) == "stop translation" 435 | 436 | 437 | @pytest.mark.parametrize( 438 | "code,message", 439 | [ 440 | ("J", 'invalid IUPAC amino acid code "J"'), 441 | ("JJ", "invalid amino acid codon"), 442 | ("NNN", "ambiguous codon"), 443 | ("---", "codon contains gap"), 444 | ], 445 | ) 446 | def test__new__invalid_code(code: str, message: str) -> None: 447 | with pytest.raises(ValueError) as excinfo: 448 | haem.AminoAcid(code) 449 | 450 | assert str(excinfo.value) == message 451 | 452 | 453 | def test__repr__() -> None: 454 | assert repr(haem.AminoAcid.ALANINE) == "AminoAcid.ALANINE" 455 | 456 | 457 | @pytest.mark.parametrize( 458 | "amino_acid,text", 459 | [ 460 | (haem.AminoAcid.ALANINE, "alanine"), 461 | (haem.AminoAcid.ASPARTIC_ACID_ASPARAGINE, "aspartic acid/asparagine"), 462 | (haem.AminoAcid.CYSTEINE, "cysteine"), 463 | (haem.AminoAcid.ASPARTIC_ACID, "aspartic acid"), 464 | (haem.AminoAcid.GLUTAMIC_ACID, "glutamic acid"), 465 | (haem.AminoAcid.PHENYLALANINE, "phenylalanine"), 466 | (haem.AminoAcid.GLYCINE, "glycine"), 467 | (haem.AminoAcid.HISTIDINE, "histidine"), 468 | (haem.AminoAcid.ISOLEUCINE, "isoleucine"), 469 | (haem.AminoAcid.LYSINE, "lysine"), 470 | (haem.AminoAcid.LEUCINE, "leucine"), 471 | (haem.AminoAcid.METHIONINE, "methionine"), 472 | (haem.AminoAcid.ASPARAGINE, "asparagine"), 473 | (haem.AminoAcid.PROLINE, "proline"), 474 | (haem.AminoAcid.GLUTAMINE, "glutamine"), 475 | (haem.AminoAcid.ARGININE, "arginine"), 476 | (haem.AminoAcid.SERINE, "serine"), 477 | (haem.AminoAcid.THREONINE, "threonine"), 478 | (haem.AminoAcid.VALINE, "valine"), 479 | (haem.AminoAcid.TRYPTOPHAN, "tryptophan"), 480 | (haem.AminoAcid.ANY, "any"), 481 | (haem.AminoAcid.TYROSINE, "tyrosine"), 482 | (haem.AminoAcid.GLUTAMINE_GLUTAMIC_ACID, "glutamine/glutamic acid"), 483 | ], 484 | ) 485 | def test__str__(amino_acid: haem.AminoAcid, text: str) -> None: 486 | assert str(amino_acid) == text 487 | 488 | 489 | @pytest.mark.parametrize( 490 | "amino_acid,code", 491 | [ 492 | (haem.AminoAcid.ALANINE, "A"), 493 | (haem.AminoAcid.ASPARTIC_ACID_ASPARAGINE, "B"), 494 | (haem.AminoAcid.CYSTEINE, "C"), 495 | (haem.AminoAcid.ASPARTIC_ACID, "D"), 496 | (haem.AminoAcid.GLUTAMIC_ACID, "E"), 497 | (haem.AminoAcid.PHENYLALANINE, "F"), 498 | (haem.AminoAcid.GLYCINE, "G"), 499 | (haem.AminoAcid.HISTIDINE, "H"), 500 | (haem.AminoAcid.ISOLEUCINE, "I"), 501 | (haem.AminoAcid.LYSINE, "K"), 502 | (haem.AminoAcid.LEUCINE, "L"), 503 | (haem.AminoAcid.METHIONINE, "M"), 504 | (haem.AminoAcid.ASPARAGINE, "N"), 505 | (haem.AminoAcid.PROLINE, "P"), 506 | (haem.AminoAcid.GLUTAMINE, "Q"), 507 | (haem.AminoAcid.ARGININE, "R"), 508 | (haem.AminoAcid.SERINE, "S"), 509 | (haem.AminoAcid.THREONINE, "T"), 510 | (haem.AminoAcid.VALINE, "V"), 511 | (haem.AminoAcid.TRYPTOPHAN, "W"), 512 | (haem.AminoAcid.ANY, "X"), 513 | (haem.AminoAcid.TYROSINE, "Y"), 514 | (haem.AminoAcid.GLUTAMINE_GLUTAMIC_ACID, "Z"), 515 | ], 516 | ) 517 | def test_code(amino_acid: haem.AminoAcid, code: str) -> None: 518 | assert amino_acid.code == code 519 | 520 | 521 | @pytest.mark.parametrize( 522 | "amino_acid,short_name", 523 | [ 524 | (haem.AminoAcid.ALANINE, "ala"), 525 | (haem.AminoAcid.ASPARTIC_ACID_ASPARAGINE, "asx"), 526 | (haem.AminoAcid.CYSTEINE, "cys"), 527 | (haem.AminoAcid.ASPARTIC_ACID, "asp"), 528 | (haem.AminoAcid.GLUTAMIC_ACID, "glu"), 529 | (haem.AminoAcid.PHENYLALANINE, "phe"), 530 | (haem.AminoAcid.GLYCINE, "gly"), 531 | (haem.AminoAcid.HISTIDINE, "his"), 532 | (haem.AminoAcid.ISOLEUCINE, "ile"), 533 | (haem.AminoAcid.LYSINE, "lys"), 534 | (haem.AminoAcid.LEUCINE, "leu"), 535 | (haem.AminoAcid.METHIONINE, "met"), 536 | (haem.AminoAcid.ASPARAGINE, "asn"), 537 | (haem.AminoAcid.PROLINE, "pro"), 538 | (haem.AminoAcid.GLUTAMINE, "gln"), 539 | (haem.AminoAcid.ARGININE, "arg"), 540 | (haem.AminoAcid.SERINE, "ser"), 541 | (haem.AminoAcid.THREONINE, "thr"), 542 | (haem.AminoAcid.VALINE, "val"), 543 | (haem.AminoAcid.TRYPTOPHAN, "trp"), 544 | (haem.AminoAcid.ANY, "xaa"), 545 | (haem.AminoAcid.TYROSINE, "tyr"), 546 | (haem.AminoAcid.GLUTAMINE_GLUTAMIC_ACID, "glx"), 547 | ], 548 | ) 549 | def test_short_name(amino_acid: haem.AminoAcid, short_name: str) -> None: 550 | assert amino_acid.short_name == short_name 551 | 552 | 553 | def test__eq__() -> None: 554 | assert haem.AminoAcid.ALANINE == haem.AminoAcid.ALANINE 555 | 556 | 557 | def test__ne__() -> None: 558 | assert haem.AminoAcid.ALANINE != haem.AminoAcid.ARGININE 559 | 560 | 561 | @pytest.mark.parametrize("op", [operator.gt, operator.ge, operator.lt, operator.le]) 562 | def test_unsupported_comparison( 563 | op: typing.Callable[[haem.AminoAcid, haem.AminoAcid], bool], 564 | ) -> None: 565 | with pytest.raises(TypeError): 566 | op(haem.AminoAcid.ALANINE, haem.AminoAcid.ARGININE) 567 | 568 | 569 | def test__bool__() -> None: 570 | assert bool(haem.AminoAcid.ALANINE) is True 571 | 572 | 573 | @pytest.mark.parametrize( 574 | "left,right,result", 575 | [ 576 | (haem.AminoAcid("M"), haem.AminoAcid("V"), haem.AminoAcidSequence("MV")), 577 | ( 578 | haem.AminoAcid("M"), 579 | haem.AminoAcidSequence("VVR"), 580 | haem.AminoAcidSequence("MVVR"), 581 | ), 582 | (haem.AminoAcid("M"), haem.AminoAcidSequence(), haem.AminoAcidSequence("M")), 583 | (haem.AminoAcid("V"), "M", haem.AminoAcidSequence("VM")), 584 | (haem.AminoAcid("V"), "", haem.AminoAcidSequence("V")), 585 | ], 586 | ) 587 | def test__add__( 588 | left: haem.AminoAcid, 589 | right: typing.Union[haem.AminoAcid, haem.AminoAcidSequence, str], 590 | result: haem.AminoAcidSequence, 591 | ) -> None: 592 | assert left + right == result 593 | 594 | 595 | @pytest.mark.parametrize( 596 | "left,right,result", 597 | [ 598 | ( 599 | haem.AminoAcidSequence("VVR"), 600 | haem.AminoAcid("M"), 601 | haem.AminoAcidSequence("VVRM"), 602 | ), 603 | (haem.AminoAcidSequence(), haem.AminoAcid("M"), haem.AminoAcidSequence("M")), 604 | ("V", haem.AminoAcid("M"), haem.AminoAcidSequence("VM")), 605 | ("", haem.AminoAcid("M"), haem.AminoAcidSequence("M")), 606 | ], 607 | ) 608 | def test__radd__( 609 | left: typing.Union[haem.AminoAcidSequence, str], 610 | right: haem.AminoAcid, 611 | result: haem.AminoAcidSequence, 612 | ) -> None: 613 | assert left + right == result 614 | -------------------------------------------------------------------------------- /tests/test_amino_acid_sequence.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import typing 3 | 4 | import pytest 5 | 6 | import haem 7 | 8 | 9 | def test__new__str() -> None: 10 | assert haem.AminoAcidSequence("MVVR") == haem.AminoAcidSequence( 11 | [ 12 | haem.AminoAcid.METHIONINE, 13 | haem.AminoAcid.VALINE, 14 | haem.AminoAcid.VALINE, 15 | haem.AminoAcid.ARGININE, 16 | ] 17 | ) 18 | 19 | 20 | def test__new__str__invalid() -> None: 21 | with pytest.raises(ValueError) as excinfo: 22 | haem.AminoAcidSequence("JJ") 23 | 24 | assert str(excinfo.value) == 'invalid IUPAC amino acid code "J"' 25 | 26 | 27 | def test__new__iterable_amino_acid() -> None: 28 | assert haem.AminoAcidSequence( 29 | iter( 30 | [ 31 | haem.AminoAcid.METHIONINE, 32 | haem.AminoAcid.VALINE, 33 | haem.AminoAcid.VALINE, 34 | haem.AminoAcid.ARGININE, 35 | ] 36 | ) 37 | ) == haem.AminoAcidSequence("MVVR") 38 | 39 | 40 | def test__new__iterable_str() -> None: 41 | assert haem.AminoAcidSequence(iter(["M", "V", "V", "R"])) == haem.AminoAcidSequence( 42 | "MVVR" 43 | ) 44 | 45 | 46 | def test__new__iterable_invalid() -> None: 47 | with pytest.raises(ValueError) as excinfo: 48 | haem.AminoAcidSequence(iter(["J"])) 49 | 50 | assert str(excinfo.value) == 'invalid IUPAC amino acid code "J"' 51 | 52 | 53 | def test__new__sequence_amino_acids() -> None: 54 | assert haem.AminoAcidSequence( 55 | [ 56 | haem.AminoAcid.METHIONINE, 57 | haem.AminoAcid.VALINE, 58 | haem.AminoAcid.VALINE, 59 | haem.AminoAcid.ARGININE, 60 | ] 61 | ) == haem.AminoAcidSequence("MVVR") 62 | 63 | 64 | def test__new__sequence_str() -> None: 65 | assert haem.AminoAcidSequence(["M", "V", "V", "R"]) == haem.AminoAcidSequence( 66 | "MVVR" 67 | ) 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "amino_acids,text", 72 | [ 73 | ([], ""), 74 | ([haem.AminoAcid.METHIONINE], ""), 75 | ( 76 | [haem.AminoAcid.METHIONINE, haem.AminoAcid.ARGININE], 77 | "", 78 | ), 79 | ( 80 | [haem.AminoAcid.METHIONINE for _ in range(100)], 81 | f"", 82 | ), 83 | ], 84 | ) 85 | def test__repr__(amino_acids: typing.List[haem.AminoAcid], text: str) -> None: 86 | assert repr(haem.AminoAcidSequence(amino_acids)) == text 87 | 88 | 89 | @pytest.mark.parametrize( 90 | "amino_acids,text", 91 | [ 92 | ([], ""), 93 | ([haem.AminoAcid.METHIONINE], "M"), 94 | ([haem.AminoAcid.METHIONINE, haem.AminoAcid.ARGININE], "MR"), 95 | ([haem.AminoAcid.VALINE for _ in range(20)], "V" * 20), 96 | ( 97 | [haem.AminoAcid.VALINE for _ in range(21)] + [haem.AminoAcid.METHIONINE], 98 | f"{'V' * 10}...{'V' * 9}M", 99 | ), 100 | ], 101 | ) 102 | def test__str__(amino_acids: typing.List[haem.AminoAcid], text: str) -> None: 103 | assert str(haem.AminoAcidSequence(amino_acids)) == text 104 | 105 | 106 | @pytest.mark.parametrize( 107 | "amino_acids", [[], [haem.AminoAcid.METHIONINE, haem.AminoAcid.VALINE]] 108 | ) 109 | def test__eq__(amino_acids: typing.List[haem.AminoAcid]) -> None: 110 | assert haem.AminoAcidSequence(amino_acids) == haem.AminoAcidSequence(amino_acids) 111 | 112 | 113 | def test__ne__() -> None: 114 | assert haem.AminoAcidSequence( 115 | [haem.AminoAcid.METHIONINE, haem.AminoAcid.VALINE] 116 | ) != haem.AminoAcidSequence([haem.AminoAcid.METHIONINE, haem.AminoAcid.ARGININE]) 117 | 118 | 119 | @pytest.mark.parametrize("op", [operator.gt, operator.ge, operator.lt, operator.le]) 120 | def test_unsupported_comparison( 121 | op: typing.Callable[[haem.AminoAcidSequence, haem.AminoAcidSequence], bool], 122 | ) -> None: 123 | with pytest.raises(TypeError): 124 | op(haem.AminoAcidSequence(), haem.AminoAcidSequence()) 125 | 126 | 127 | @pytest.mark.parametrize( 128 | "amino_acids,result", [([], False), ([haem.AminoAcid.METHIONINE], True)] 129 | ) 130 | def test__bool__(amino_acids: typing.List[haem.AminoAcid], result: bool) -> None: 131 | assert bool(haem.AminoAcidSequence(amino_acids)) is result 132 | 133 | 134 | @pytest.mark.parametrize( 135 | "left,right,result", 136 | [ 137 | ( 138 | haem.AminoAcidSequence("MV"), 139 | haem.AminoAcid("R"), 140 | haem.AminoAcidSequence("MVR"), 141 | ), 142 | ( 143 | haem.AminoAcidSequence("MV"), 144 | haem.AminoAcidSequence("RVR"), 145 | haem.AminoAcidSequence("MVRVR"), 146 | ), 147 | ( 148 | haem.AminoAcidSequence("MV"), 149 | haem.AminoAcidSequence(), 150 | haem.AminoAcidSequence("MV"), 151 | ), 152 | (haem.AminoAcidSequence(), haem.AminoAcidSequence(), haem.AminoAcidSequence()), 153 | (haem.AminoAcidSequence(), haem.AminoAcid("M"), haem.AminoAcidSequence("M")), 154 | (haem.AminoAcidSequence(), "", haem.AminoAcidSequence()), 155 | (haem.AminoAcidSequence(), "R", haem.AminoAcidSequence("R")), 156 | (haem.AminoAcidSequence("MV"), "R", haem.AminoAcidSequence("MVR")), 157 | (haem.AminoAcidSequence("MV"), "", haem.AminoAcidSequence("MV")), 158 | ], 159 | ) 160 | def test__add__( 161 | left: haem.AminoAcidSequence, 162 | right: typing.Union[haem.AminoAcid, haem.AminoAcidSequence, str], 163 | result: haem.AminoAcidSequence, 164 | ) -> None: 165 | assert left + right == result 166 | 167 | 168 | @pytest.mark.parametrize( 169 | "left,right,result", 170 | [ 171 | ( 172 | haem.AminoAcid("R"), 173 | haem.AminoAcidSequence("MV"), 174 | haem.AminoAcidSequence("RMV"), 175 | ), 176 | (haem.AminoAcid("M"), haem.AminoAcidSequence(), haem.AminoAcidSequence("M")), 177 | ("", haem.AminoAcidSequence(), haem.AminoAcidSequence()), 178 | ("R", haem.AminoAcidSequence(), haem.AminoAcidSequence("R")), 179 | ("R", haem.AminoAcidSequence("MV"), haem.AminoAcidSequence("RMV")), 180 | ("", haem.AminoAcidSequence("MV"), haem.AminoAcidSequence("MV")), 181 | ], 182 | ) 183 | def test__radd__( 184 | left: typing.Union[haem.AminoAcid, str], 185 | right: haem.AminoAcidSequence, 186 | result: haem.AminoAcidSequence, 187 | ) -> None: 188 | assert left + right == result 189 | 190 | 191 | @pytest.mark.parametrize( 192 | "sequence,target,result", 193 | [ 194 | (haem.AminoAcidSequence("M"), haem.AminoAcid.METHIONINE, True), 195 | (haem.AminoAcidSequence(), haem.AminoAcid.METHIONINE, False), 196 | (haem.AminoAcidSequence("V"), haem.AminoAcid.METHIONINE, False), 197 | (haem.AminoAcidSequence("MVV"), haem.AminoAcid.VALINE, True), 198 | (haem.AminoAcidSequence("MVV"), haem.AminoAcid.ARGININE, False), 199 | (haem.AminoAcidSequence(), haem.AminoAcidSequence(), True), 200 | (haem.AminoAcidSequence("MVR"), haem.AminoAcidSequence("VR"), True), 201 | (haem.AminoAcidSequence("MVR"), haem.AminoAcidSequence("RV"), False), 202 | (haem.AminoAcidSequence("M"), haem.AminoAcidSequence("MM"), False), 203 | (haem.AminoAcidSequence(), "", True), 204 | (haem.AminoAcidSequence("MVR"), "VR", True), 205 | (haem.AminoAcidSequence("MVR"), "RV", False), 206 | (haem.AminoAcidSequence("M"), "MM", False), 207 | ], 208 | ) 209 | def test__contains__( 210 | sequence: haem.AminoAcidSequence, 211 | target: typing.Union[haem.AminoAcid, haem.AminoAcidSequence], 212 | result: bool, 213 | ) -> None: 214 | assert (target in sequence) is result 215 | 216 | 217 | @pytest.mark.parametrize("amino_acids,length", [([], 0), ([haem.AminoAcid.VALINE], 1)]) 218 | def test__len__(amino_acids: typing.List[haem.AminoAcid], length: int) -> None: 219 | assert len(haem.AminoAcidSequence(amino_acids)) == length 220 | 221 | 222 | @pytest.mark.parametrize( 223 | "sequence,index,amino_acid", 224 | [ 225 | (haem.AminoAcidSequence("MVR"), 0, haem.AminoAcid.METHIONINE), 226 | (haem.AminoAcidSequence("MVR"), 1, haem.AminoAcid.VALINE), 227 | (haem.AminoAcidSequence("MVR"), 2, haem.AminoAcid.ARGININE), 228 | (haem.AminoAcidSequence("MVR"), -1, haem.AminoAcid.ARGININE), 229 | (haem.AminoAcidSequence("MVR"), -2, haem.AminoAcid.VALINE), 230 | (haem.AminoAcidSequence("MVR"), -3, haem.AminoAcid.METHIONINE), 231 | ], 232 | ) 233 | def test__getitem__index( 234 | sequence: haem.AminoAcidSequence, index: int, amino_acid: haem.AminoAcid 235 | ) -> None: 236 | assert sequence[index] == amino_acid 237 | 238 | 239 | @pytest.mark.parametrize("index", [3, -4]) 240 | def test__getitem__index_out_of_range(index: int) -> None: 241 | with pytest.raises(IndexError) as excinfo: 242 | haem.AminoAcidSequence("MVR")[index] 243 | 244 | assert str(excinfo.value) == "AminoAcidSequence index out of range" 245 | 246 | 247 | @pytest.mark.parametrize( 248 | "sequence,slic,result", 249 | [ 250 | (haem.AminoAcidSequence(), slice(0, 0), haem.AminoAcidSequence()), 251 | (haem.AminoAcidSequence("MVR"), slice(0, 2), haem.AminoAcidSequence("MV")), 252 | (haem.AminoAcidSequence("MVR"), slice(1, 3), haem.AminoAcidSequence("VR")), 253 | (haem.AminoAcidSequence("MVR"), slice(0, 3), haem.AminoAcidSequence("MVR")), 254 | (haem.AminoAcidSequence("MVR"), slice(0, 4), haem.AminoAcidSequence("MVR")), 255 | (haem.AminoAcidSequence("MVR"), slice(0, None), haem.AminoAcidSequence("MVR")), 256 | (haem.AminoAcidSequence("MVR"), slice(1, None), haem.AminoAcidSequence("VR")), 257 | (haem.AminoAcidSequence("MVR"), slice(-1, None), haem.AminoAcidSequence("R")), 258 | (haem.AminoAcidSequence("MVR"), slice(-3, None), haem.AminoAcidSequence("MVR")), 259 | (haem.AminoAcidSequence("MVR"), slice(-4, None), haem.AminoAcidSequence("MVR")), 260 | (haem.AminoAcidSequence("MVR"), slice(0, -1), haem.AminoAcidSequence("MV")), 261 | ( 262 | haem.AminoAcidSequence("MVRVRV"), 263 | slice(0, -1, 2), 264 | haem.AminoAcidSequence("MRR"), 265 | ), 266 | ( 267 | haem.AminoAcidSequence("MVRVRV"), 268 | slice(5, None, -1), 269 | haem.AminoAcidSequence("VRVRVM"), 270 | ), 271 | ( 272 | haem.AminoAcidSequence("MVRVRV"), 273 | slice(None, None, -1), 274 | haem.AminoAcidSequence("VRVRVM"), 275 | ), 276 | ( 277 | haem.AminoAcidSequence("MVRVRV"), 278 | slice(10, 2, -2), 279 | haem.AminoAcidSequence("VV"), 280 | ), 281 | ], 282 | ) 283 | def test__getitem__slice( 284 | sequence: haem.AminoAcidSequence, slic: slice, result: haem.AminoAcidSequence 285 | ) -> None: 286 | assert sequence[slic.start : slic.stop : slic.step] == result 287 | 288 | 289 | @pytest.mark.parametrize( 290 | "amino_acids", 291 | [[haem.AminoAcid("M")], [haem.AminoAcid("V"), haem.AminoAcid("R")], []], 292 | ) 293 | def test__iter__(amino_acids: typing.List[haem.AminoAcid]) -> None: 294 | sequence_iter = iter(haem.AminoAcidSequence(amino_acids)) 295 | 296 | for amino_acid in amino_acids: 297 | assert next(sequence_iter) == amino_acid 298 | 299 | with pytest.raises(StopIteration): 300 | next(sequence_iter) 301 | 302 | 303 | @pytest.mark.parametrize( 304 | "sequence,target,total", 305 | [ 306 | (haem.AminoAcidSequence(), haem.AminoAcid.VALINE, 0), 307 | (haem.AminoAcidSequence("MVRV"), haem.AminoAcid.VALINE, 2), 308 | (haem.AminoAcidSequence("MVRV"), "V", 2), 309 | ], 310 | ) 311 | def test_count( 312 | sequence: haem.AminoAcidSequence, 313 | target: typing.Union[haem.AminoAcid, str], 314 | total: int, 315 | ) -> None: 316 | assert sequence.count(target) == total 317 | 318 | 319 | @pytest.mark.parametrize( 320 | "sequence,target,total", 321 | [ 322 | (haem.AminoAcidSequence(), haem.AminoAcidSequence("VR"), 0), 323 | (haem.AminoAcidSequence("MVRV"), haem.AminoAcidSequence("VR"), 1), 324 | (haem.AminoAcidSequence("MVMV"), haem.AminoAcidSequence("MV"), 2), 325 | (haem.AminoAcidSequence(), "VR", 0), 326 | (haem.AminoAcidSequence("MVRV"), "VR", 1), 327 | (haem.AminoAcidSequence("MVMV"), "MV", 2), 328 | (haem.AminoAcidSequence(), haem.AminoAcidSequence(), 0), 329 | (haem.AminoAcidSequence("MVRV"), haem.AminoAcidSequence(), 0), 330 | (haem.AminoAcidSequence(), "", 0), 331 | (haem.AminoAcidSequence("MVRV"), "", 0), 332 | ], 333 | ) 334 | def test_count_sequence( 335 | sequence: haem.AminoAcidSequence, 336 | target: typing.Union[haem.AminoAcidSequence, str], 337 | total: int, 338 | ) -> None: 339 | assert sequence.count(target) == total 340 | 341 | 342 | @pytest.mark.parametrize( 343 | "sequence,target,total", 344 | [ 345 | (haem.AminoAcidSequence("MRRR"), haem.AminoAcidSequence("RR"), 2), 346 | (haem.AminoAcidSequence("MRRA"), haem.AminoAcidSequence("RR"), 1), 347 | ], 348 | ) 349 | def test_count_overlap( 350 | sequence: haem.AminoAcidSequence, 351 | target: typing.Union[haem.AminoAcidSequence, str], 352 | total: int, 353 | ) -> None: 354 | assert sequence.count(target, overlap=True) == total 355 | 356 | 357 | @pytest.mark.parametrize( 358 | "sequence,target,result", 359 | [ 360 | (haem.AminoAcidSequence(), haem.AminoAcidSequence(), None), 361 | (haem.AminoAcidSequence(), haem.AminoAcid("A"), None), 362 | (haem.AminoAcidSequence(), "", None), 363 | (haem.AminoAcidSequence(), "AR", None), 364 | (haem.AminoAcidSequence("MVR"), haem.AminoAcidSequence(), None), 365 | (haem.AminoAcidSequence("MVR"), haem.AminoAcid("N"), None), 366 | (haem.AminoAcidSequence("MVR"), "", None), 367 | (haem.AminoAcidSequence("MVR"), "RV", None), 368 | (haem.AminoAcidSequence("MVR"), haem.AminoAcidSequence("VR"), 1), 369 | (haem.AminoAcidSequence("MVR"), haem.AminoAcid("R"), 2), 370 | (haem.AminoAcidSequence("MVR"), "VR", 1), 371 | ], 372 | ) 373 | def test_find( 374 | sequence: haem.AminoAcidSequence, 375 | target: typing.Union[haem.AminoAcidSequence, haem.AminoAcid, str], 376 | result: typing.Optional[int], 377 | ) -> None: 378 | assert sequence.find(target) == result 379 | -------------------------------------------------------------------------------- /tests/test_dna_base.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import typing 3 | 4 | import pytest 5 | 6 | import haem 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "code,base", 11 | [ 12 | ("A", haem.DNABase.ADENINE), 13 | ("C", haem.DNABase.CYTOSINE), 14 | ("G", haem.DNABase.GUANINE), 15 | ("T", haem.DNABase.THYMINE), 16 | ("M", haem.DNABase.ADENINE_CYTOSINE), 17 | ("R", haem.DNABase.ADENINE_GUANINE), 18 | ("W", haem.DNABase.ADENINE_THYMINE), 19 | ("S", haem.DNABase.CYTOSINE_GUANINE), 20 | ("Y", haem.DNABase.CYTOSINE_THYMINE), 21 | ("K", haem.DNABase.GUANINE_THYMINE), 22 | ("V", haem.DNABase.ADENINE_CYTOSINE_GUANINE), 23 | ("H", haem.DNABase.ADENINE_CYTOSINE_THYMINE), 24 | ("D", haem.DNABase.ADENINE_GUANINE_THYMINE), 25 | ("B", haem.DNABase.CYTOSINE_GUANINE_THYMINE), 26 | ("N", haem.DNABase.ANY), 27 | (".", haem.DNABase.GAP), 28 | ("-", haem.DNABase.GAP), 29 | ], 30 | ) 31 | def test__new__(code: str, base: haem.DNABase) -> None: 32 | assert haem.DNABase(code) == base 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "code,message", 37 | [("X", 'invalid IUPAC DNA code "X"'), ("XX", "expected a string of length 1")], 38 | ) 39 | def test__new__invalid_code(code: str, message: str) -> None: 40 | with pytest.raises(ValueError) as excinfo: 41 | haem.DNABase(code) 42 | 43 | assert str(excinfo.value) == message 44 | 45 | 46 | def test__repr__() -> None: 47 | assert repr(haem.DNABase.ADENINE) == "DNABase.ADENINE" 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "base,text", 52 | [ 53 | (haem.DNABase.ADENINE, "adenine"), 54 | (haem.DNABase.CYTOSINE, "cytosine"), 55 | (haem.DNABase.GUANINE, "guanine"), 56 | (haem.DNABase.THYMINE, "thymine"), 57 | (haem.DNABase.ADENINE_CYTOSINE, "adenine/cytosine"), 58 | (haem.DNABase.ADENINE_GUANINE, "adenine/guanine"), 59 | (haem.DNABase.ADENINE_THYMINE, "adenine/thymine"), 60 | (haem.DNABase.CYTOSINE_GUANINE, "cytosine/guanine"), 61 | (haem.DNABase.CYTOSINE_THYMINE, "cytosine/thymine"), 62 | (haem.DNABase.GUANINE_THYMINE, "guanine/thymine"), 63 | (haem.DNABase.ADENINE_CYTOSINE_GUANINE, "adenine/cytosine/guanine"), 64 | (haem.DNABase.ADENINE_CYTOSINE_THYMINE, "adenine/cytosine/thymine"), 65 | (haem.DNABase.ADENINE_GUANINE_THYMINE, "adenine/guanine/thymine"), 66 | (haem.DNABase.CYTOSINE_GUANINE_THYMINE, "cytosine/guanine/thymine"), 67 | (haem.DNABase.ANY, "any"), 68 | (haem.DNABase.GAP, "gap"), 69 | ], 70 | ) 71 | def test__str__(base: haem.DNABase, text: str) -> None: 72 | assert str(base) == text 73 | 74 | 75 | @pytest.mark.parametrize( 76 | "base,code", 77 | [ 78 | (haem.DNABase.ADENINE, "A"), 79 | (haem.DNABase.CYTOSINE, "C"), 80 | (haem.DNABase.GUANINE, "G"), 81 | (haem.DNABase.THYMINE, "T"), 82 | (haem.DNABase.ADENINE_CYTOSINE, "M"), 83 | (haem.DNABase.ADENINE_GUANINE, "R"), 84 | (haem.DNABase.ADENINE_THYMINE, "W"), 85 | (haem.DNABase.CYTOSINE_GUANINE, "S"), 86 | (haem.DNABase.CYTOSINE_THYMINE, "Y"), 87 | (haem.DNABase.GUANINE_THYMINE, "K"), 88 | (haem.DNABase.ADENINE_CYTOSINE_GUANINE, "V"), 89 | (haem.DNABase.ADENINE_CYTOSINE_THYMINE, "H"), 90 | (haem.DNABase.ADENINE_GUANINE_THYMINE, "D"), 91 | (haem.DNABase.CYTOSINE_GUANINE_THYMINE, "B"), 92 | (haem.DNABase.ANY, "N"), 93 | (haem.DNABase.GAP, "-"), 94 | ], 95 | ) 96 | def test_code(base: haem.DNABase, code: str) -> None: 97 | assert base.code == code 98 | 99 | 100 | @pytest.mark.parametrize( 101 | "base,complement", 102 | [ 103 | (haem.DNABase.ADENINE, haem.DNABase.THYMINE), 104 | (haem.DNABase.CYTOSINE, haem.DNABase.GUANINE), 105 | (haem.DNABase.GUANINE, haem.DNABase.CYTOSINE), 106 | (haem.DNABase.THYMINE, haem.DNABase.ADENINE), 107 | (haem.DNABase.ADENINE_CYTOSINE, haem.DNABase.GUANINE_THYMINE), 108 | (haem.DNABase.ADENINE_GUANINE, haem.DNABase.CYTOSINE_THYMINE), 109 | (haem.DNABase.ADENINE_THYMINE, haem.DNABase.ADENINE_THYMINE), 110 | (haem.DNABase.CYTOSINE_GUANINE, haem.DNABase.CYTOSINE_GUANINE), 111 | (haem.DNABase.CYTOSINE_THYMINE, haem.DNABase.ADENINE_GUANINE), 112 | (haem.DNABase.GUANINE_THYMINE, haem.DNABase.ADENINE_CYTOSINE), 113 | (haem.DNABase.ADENINE_CYTOSINE_GUANINE, haem.DNABase.CYTOSINE_GUANINE_THYMINE), 114 | (haem.DNABase.ADENINE_CYTOSINE_THYMINE, haem.DNABase.ADENINE_GUANINE_THYMINE), 115 | (haem.DNABase.ADENINE_GUANINE_THYMINE, haem.DNABase.ADENINE_CYTOSINE_THYMINE), 116 | (haem.DNABase.CYTOSINE_GUANINE_THYMINE, haem.DNABase.ADENINE_CYTOSINE_GUANINE), 117 | (haem.DNABase.ANY, haem.DNABase.ANY), 118 | (haem.DNABase.GAP, haem.DNABase.GAP), 119 | ], 120 | ) 121 | def test_complement(base: haem.DNABase, complement: haem.DNABase) -> None: 122 | assert base.complement == complement 123 | assert ~base == complement 124 | 125 | 126 | @pytest.mark.parametrize( 127 | "dna_base,rna_base", 128 | [ 129 | (haem.DNABase.ADENINE, haem.RNABase.ADENINE), 130 | (haem.DNABase.CYTOSINE, haem.RNABase.CYTOSINE), 131 | (haem.DNABase.GUANINE, haem.RNABase.GUANINE), 132 | (haem.DNABase.THYMINE, haem.RNABase.URACIL), 133 | (haem.DNABase.ADENINE_CYTOSINE, haem.RNABase.ADENINE_CYTOSINE), 134 | (haem.DNABase.ADENINE_GUANINE, haem.RNABase.ADENINE_GUANINE), 135 | (haem.DNABase.ADENINE_THYMINE, haem.RNABase.ADENINE_URACIL), 136 | (haem.DNABase.CYTOSINE_GUANINE, haem.RNABase.CYTOSINE_GUANINE), 137 | (haem.DNABase.CYTOSINE_THYMINE, haem.RNABase.CYTOSINE_URACIL), 138 | (haem.DNABase.GUANINE_THYMINE, haem.RNABase.GUANINE_URACIL), 139 | (haem.DNABase.ADENINE_CYTOSINE_GUANINE, haem.RNABase.ADENINE_CYTOSINE_GUANINE), 140 | (haem.DNABase.ADENINE_CYTOSINE_THYMINE, haem.RNABase.ADENINE_CYTOSINE_URACIL), 141 | (haem.DNABase.ADENINE_GUANINE_THYMINE, haem.RNABase.ADENINE_GUANINE_URACIL), 142 | (haem.DNABase.CYTOSINE_GUANINE_THYMINE, haem.RNABase.CYTOSINE_GUANINE_URACIL), 143 | (haem.DNABase.ANY, haem.RNABase.ANY), 144 | (haem.DNABase.GAP, haem.RNABase.GAP), 145 | ], 146 | ) 147 | def test_transcribe(dna_base: haem.DNABase, rna_base: haem.RNABase) -> None: 148 | assert dna_base.transcribe() == rna_base 149 | 150 | 151 | def test__eq__() -> None: 152 | assert haem.DNABase.ADENINE == haem.DNABase.ADENINE 153 | 154 | 155 | def test__ne__() -> None: 156 | assert haem.DNABase.ADENINE != haem.DNABase.CYTOSINE 157 | 158 | 159 | @pytest.mark.parametrize("op", [operator.gt, operator.ge, operator.lt, operator.le]) 160 | def test_unsupported_comparison( 161 | op: typing.Callable[[haem.DNABase, haem.DNABase], bool], 162 | ) -> None: 163 | with pytest.raises(TypeError): 164 | op(haem.DNABase.ADENINE, haem.DNABase.CYTOSINE) 165 | 166 | 167 | @pytest.mark.parametrize( 168 | "base,result", [(haem.DNABase.ADENINE, True), (haem.DNABase.GAP, False)] 169 | ) 170 | def test__bool__(base: haem.DNABase, result: bool) -> None: 171 | assert bool(base) == result 172 | 173 | 174 | @pytest.mark.parametrize( 175 | "left,right,result", 176 | [ 177 | (haem.DNABase("A"), haem.DNABase("-"), haem.DNASequence("A-")), 178 | (haem.DNABase("A"), haem.DNASequence("CTT"), haem.DNASequence("ACTT")), 179 | (haem.DNABase("A"), haem.DNASequence(), haem.DNASequence("A")), 180 | (haem.DNABase("A"), "", haem.DNASequence("A")), 181 | (haem.DNABase("A"), haem.DNASequence("T"), haem.DNASequence("AT")), 182 | (haem.DNABase("A"), iter("ACG"), haem.DNASequence("AACG")), 183 | (haem.DNABase("A"), ["A", "C", "G"], haem.DNASequence("AACG")), 184 | ( 185 | haem.DNABase.ADENINE, 186 | [haem.DNABase.ADENINE, haem.DNABase.THYMINE], 187 | haem.DNASequence("AAT"), 188 | ), 189 | ( 190 | haem.DNABase.ADENINE, 191 | iter([haem.DNABase.ADENINE, haem.DNABase.THYMINE]), 192 | haem.DNASequence("AAT"), 193 | ), 194 | ], 195 | ) 196 | def test__add__( 197 | left: haem.DNABase, 198 | right: typing.Union[ 199 | haem.DNABase, 200 | haem.DNASequence, 201 | typing.Iterator[str], 202 | typing.Sequence[str], 203 | str, 204 | ], 205 | result: haem.DNASequence, 206 | ) -> None: 207 | assert left + right == result 208 | 209 | 210 | @pytest.mark.parametrize( 211 | "left,right,result", 212 | [ 213 | (haem.DNASequence("CTT"), haem.DNABase("A"), haem.DNASequence("CTTA")), 214 | (haem.DNASequence(), haem.DNABase("A"), haem.DNASequence("A")), 215 | ("", haem.DNABase("A"), haem.DNASequence("A")), 216 | ("T", haem.DNABase("A"), haem.DNASequence("TA")), 217 | (iter("ACG"), haem.DNABase("A"), haem.DNASequence("ACGA")), 218 | (["A", "C", "G"], haem.DNABase("A"), haem.DNASequence("ACGA")), 219 | ( 220 | [haem.DNABase.ADENINE, haem.DNABase.THYMINE], 221 | haem.DNABase.ADENINE, 222 | haem.DNASequence("ATA"), 223 | ), 224 | ( 225 | iter([haem.DNABase.ADENINE, haem.DNABase.THYMINE]), 226 | haem.DNABase.ADENINE, 227 | haem.DNASequence("ATA"), 228 | ), 229 | ], 230 | ) 231 | def test__radd__( 232 | left: typing.Union[haem.DNASequence, str], 233 | right: haem.DNABase, 234 | result: haem.DNASequence, 235 | ) -> None: 236 | assert left + right == result 237 | -------------------------------------------------------------------------------- /tests/test_dna_sequence.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import typing 3 | 4 | import pytest 5 | 6 | import haem 7 | 8 | 9 | def test__new__str() -> None: 10 | assert haem.DNASequence("ACGT") == haem.DNASequence( 11 | [ 12 | haem.DNABase.ADENINE, 13 | haem.DNABase.CYTOSINE, 14 | haem.DNABase.GUANINE, 15 | haem.DNABase.THYMINE, 16 | ] 17 | ) 18 | 19 | 20 | def test__new__str__invalid() -> None: 21 | with pytest.raises(ValueError) as excinfo: 22 | haem.DNASequence("ACGTX") 23 | 24 | assert str(excinfo.value) == 'invalid IUPAC DNA code "X"' 25 | 26 | 27 | def test__new__iterable_base() -> None: 28 | assert haem.DNASequence( 29 | iter( 30 | [ 31 | haem.DNABase.ADENINE, 32 | haem.DNABase.CYTOSINE, 33 | haem.DNABase.GUANINE, 34 | haem.DNABase.THYMINE, 35 | ] 36 | ) 37 | ) == haem.DNASequence("ACGT") 38 | 39 | 40 | def test__new__iterable_str() -> None: 41 | assert haem.DNASequence(iter(["A", "C", "G", "T"])) == haem.DNASequence("ACGT") 42 | 43 | 44 | def test__new__iterable_invalid() -> None: 45 | with pytest.raises(ValueError) as excinfo: 46 | haem.DNASequence(iter(["A", "C", "G", "X"])) 47 | 48 | assert str(excinfo.value) == 'invalid IUPAC DNA code "X"' 49 | 50 | 51 | def test__new__sequence_bases() -> None: 52 | assert haem.DNASequence( 53 | [ 54 | haem.DNABase.ADENINE, 55 | haem.DNABase.CYTOSINE, 56 | haem.DNABase.GUANINE, 57 | haem.DNABase.THYMINE, 58 | ] 59 | ) == haem.DNASequence("ACGT") 60 | 61 | 62 | def test__new__sequence_str() -> None: 63 | assert haem.DNASequence(["A", "C", "G", "T"]) == haem.DNASequence("ACGT") 64 | 65 | 66 | @pytest.mark.parametrize( 67 | "sequence,complement", 68 | [ 69 | (haem.DNASequence(), haem.DNASequence()), 70 | (haem.DNASequence("A"), haem.DNASequence("T")), 71 | (haem.DNASequence("ACGT"), haem.DNASequence("TGCA")), 72 | ], 73 | ) 74 | def test_complement(sequence: haem.DNASequence, complement: haem.DNASequence) -> None: 75 | assert sequence.complement == complement 76 | assert ~sequence == complement 77 | 78 | 79 | @pytest.mark.parametrize( 80 | "dna_sequence,rna_sequence", 81 | [ 82 | (haem.DNASequence(), haem.RNASequence()), 83 | (haem.DNASequence("ACGT"), haem.RNASequence("ACGU")), 84 | ], 85 | ) 86 | def test_transcribe( 87 | dna_sequence: haem.DNASequence, rna_sequence: haem.RNASequence 88 | ) -> None: 89 | assert dna_sequence.transcribe() == rna_sequence 90 | 91 | 92 | @pytest.mark.parametrize( 93 | "bases,text", 94 | [ 95 | ([], ""), 96 | ([haem.DNABase.ADENINE], ""), 97 | ([haem.DNABase.ADENINE, haem.DNABase.ADENINE_CYTOSINE], ""), 98 | ([haem.DNABase.ADENINE for _ in range(100)], f""), 99 | ], 100 | ) 101 | def test__repr__(bases: typing.List[haem.DNABase], text: str) -> None: 102 | assert repr(haem.DNASequence(bases)) == text 103 | 104 | 105 | @pytest.mark.parametrize( 106 | "bases,text", 107 | [ 108 | ([], ""), 109 | ([haem.DNABase.ADENINE], "A"), 110 | ([haem.DNABase.ADENINE, haem.DNABase.ADENINE_CYTOSINE], "AM"), 111 | ([haem.DNABase.ADENINE for _ in range(20)], "A" * 20), 112 | ( 113 | [haem.DNABase.ADENINE for _ in range(21)] + [haem.DNABase.GUANINE], 114 | f"{'A' * 10}...{'A' * 9}G", 115 | ), 116 | ], 117 | ) 118 | def test__str__(bases: typing.List[haem.DNABase], text: str) -> None: 119 | assert str(haem.DNASequence(bases)) == text 120 | 121 | 122 | @pytest.mark.parametrize("bases", [[], [haem.DNABase.ADENINE, haem.DNABase.GUANINE]]) 123 | def test__eq__(bases: typing.List[haem.DNABase]) -> None: 124 | assert haem.DNASequence(bases) == haem.DNASequence(bases) 125 | 126 | 127 | def test__ne__() -> None: 128 | assert haem.DNASequence( 129 | [haem.DNABase.ADENINE, haem.DNABase.GUANINE] 130 | ) != haem.DNASequence([haem.DNABase.ADENINE, haem.DNABase.THYMINE]) 131 | 132 | 133 | @pytest.mark.parametrize("op", [operator.gt, operator.ge, operator.lt, operator.le]) 134 | def test_unsupported_comparison( 135 | op: typing.Callable[[haem.DNASequence, haem.DNASequence], bool], 136 | ) -> None: 137 | with pytest.raises(TypeError): 138 | op(haem.DNASequence(), haem.DNASequence()) 139 | 140 | 141 | @pytest.mark.parametrize("bases,result", [([], False), ([haem.DNABase.ADENINE], True)]) 142 | def test__bool__(bases: typing.List[haem.DNABase], result: bool) -> None: 143 | assert bool(haem.DNASequence(bases)) is result 144 | 145 | 146 | @pytest.mark.parametrize( 147 | "left,right,result", 148 | [ 149 | (haem.DNASequence("A-"), haem.DNABase.GUANINE, haem.DNASequence("A-G")), 150 | (haem.DNASequence("A-"), haem.DNASequence("CTT"), haem.DNASequence("A-CTT")), 151 | (haem.DNASequence("A-"), haem.DNASequence(), haem.DNASequence("A-")), 152 | (haem.DNASequence(), haem.DNASequence(), haem.DNASequence()), 153 | (haem.DNASequence(), haem.DNABase.GUANINE, haem.DNASequence("G")), 154 | (haem.DNASequence("A-"), "", haem.DNASequence("A-")), 155 | (haem.DNASequence("A-"), "GT", haem.DNASequence("A-GT")), 156 | (haem.DNASequence(), "", haem.DNASequence()), 157 | (haem.DNASequence(), "TG", haem.DNASequence("TG")), 158 | ], 159 | ) 160 | def test__add__( 161 | left: haem.DNASequence, 162 | right: typing.Union[haem.DNABase, haem.DNASequence, str], 163 | result: haem.DNASequence, 164 | ) -> None: 165 | assert left + right == result 166 | 167 | 168 | @pytest.mark.parametrize( 169 | "left,right,result", 170 | [ 171 | (haem.DNABase("G"), haem.DNASequence("A-"), haem.DNASequence("GA-")), 172 | (haem.DNABase("G"), haem.DNASequence(), haem.DNASequence("G")), 173 | ("", haem.DNASequence("A-"), haem.DNASequence("A-")), 174 | ("GT", haem.DNASequence("A-"), haem.DNASequence("GTA-")), 175 | ("", haem.DNASequence(), haem.DNASequence()), 176 | ("TG", haem.DNASequence(), haem.DNASequence("TG")), 177 | ], 178 | ) 179 | def test__radd__( 180 | left: typing.Union[haem.DNABase, str], 181 | right: haem.DNASequence, 182 | result: haem.DNASequence, 183 | ) -> None: 184 | assert left + right == result 185 | 186 | 187 | @pytest.mark.parametrize( 188 | "sequence,target,result", 189 | [ 190 | (haem.DNASequence("A"), haem.DNABase.ADENINE, True), 191 | (haem.DNASequence(), haem.DNABase.ADENINE, False), 192 | (haem.DNASequence("G"), haem.DNABase.ADENINE, False), 193 | (haem.DNASequence("AGCG"), haem.DNABase.GUANINE, True), 194 | (haem.DNASequence("AGCG"), haem.DNABase.THYMINE, False), 195 | (haem.DNASequence(), haem.DNASequence(), True), 196 | (haem.DNASequence("AGT"), haem.DNASequence("GT"), True), 197 | (haem.DNASequence("AGT"), haem.DNASequence("TG"), False), 198 | (haem.DNASequence("A"), haem.DNASequence("AA"), False), 199 | (haem.DNASequence(), "", True), 200 | (haem.DNASequence("AGT"), "GT", True), 201 | (haem.DNASequence("AGT"), "TG", False), 202 | (haem.DNASequence("A"), "AA", False), 203 | ], 204 | ) 205 | def test__contains__( 206 | sequence: haem.DNASequence, 207 | target: typing.Union[haem.DNABase, haem.DNASequence], 208 | result: bool, 209 | ) -> None: 210 | assert (target in sequence) is result 211 | 212 | 213 | @pytest.mark.parametrize("bases,length", [([], 0), ([haem.DNABase.ADENINE], 1)]) 214 | def test__len__(bases: typing.List[haem.DNABase], length: int) -> None: 215 | assert len(haem.DNASequence(bases)) == length 216 | 217 | 218 | @pytest.mark.parametrize( 219 | "sequence,index,base", 220 | [ 221 | (haem.DNASequence("GAT"), 0, haem.DNABase.GUANINE), 222 | (haem.DNASequence("GAT"), 1, haem.DNABase.ADENINE), 223 | (haem.DNASequence("GAT"), 2, haem.DNABase.THYMINE), 224 | (haem.DNASequence("GAT"), -1, haem.DNABase.THYMINE), 225 | (haem.DNASequence("GAT"), -2, haem.DNABase.ADENINE), 226 | (haem.DNASequence("GAT"), -3, haem.DNABase.GUANINE), 227 | ], 228 | ) 229 | def test__getitem__index( 230 | sequence: haem.DNASequence, index: int, base: haem.DNABase 231 | ) -> None: 232 | assert sequence[index] == base 233 | 234 | 235 | @pytest.mark.parametrize("index", [3, -4]) 236 | def test__getitem__index_out_of_range(index: int) -> None: 237 | with pytest.raises(IndexError) as excinfo: 238 | haem.DNASequence("GAT")[index] 239 | 240 | assert str(excinfo.value) == "DNASequence index out of range" 241 | 242 | 243 | @pytest.mark.parametrize( 244 | "sequence,slic,result", 245 | [ 246 | (haem.DNASequence(), slice(0, 0), haem.DNASequence()), 247 | (haem.DNASequence("GAT"), slice(0, 2), haem.DNASequence("GA")), 248 | (haem.DNASequence("GAT"), slice(1, 3), haem.DNASequence("AT")), 249 | (haem.DNASequence("GAT"), slice(0, 3), haem.DNASequence("GAT")), 250 | (haem.DNASequence("GAT"), slice(0, 4), haem.DNASequence("GAT")), 251 | (haem.DNASequence("GAT"), slice(0, None), haem.DNASequence("GAT")), 252 | (haem.DNASequence("GAT"), slice(1, None), haem.DNASequence("AT")), 253 | (haem.DNASequence("GAT"), slice(-1, None), haem.DNASequence("T")), 254 | (haem.DNASequence("GAT"), slice(-3, None), haem.DNASequence("GAT")), 255 | (haem.DNASequence("GAT"), slice(-4, None), haem.DNASequence("GAT")), 256 | (haem.DNASequence("GAT"), slice(0, -1), haem.DNASequence("GA")), 257 | (haem.DNASequence("GATCCA"), slice(0, -1, 2), haem.DNASequence("GTC")), 258 | (haem.DNASequence("GATCCA"), slice(5, None, -1), haem.DNASequence("ACCTAG")), 259 | (haem.DNASequence("GATCCA"), slice(None, None, -1), haem.DNASequence("ACCTAG")), 260 | (haem.DNASequence("GATCCA"), slice(10, 2, -2), haem.DNASequence("AC")), 261 | ], 262 | ) 263 | def test__getitem__slice( 264 | sequence: haem.DNASequence, slic: slice, result: haem.DNASequence 265 | ) -> None: 266 | assert sequence[slic.start : slic.stop : slic.step] == result 267 | 268 | 269 | @pytest.mark.parametrize( 270 | "bases", [[haem.DNABase("T")], [haem.DNABase("A"), haem.DNABase("C")], []] 271 | ) 272 | def test__iter__(bases: typing.List[haem.DNABase]) -> None: 273 | sequence_iter = iter(haem.DNASequence(bases)) 274 | 275 | for base in bases: 276 | assert next(sequence_iter) == base 277 | 278 | with pytest.raises(StopIteration): 279 | next(sequence_iter) 280 | 281 | 282 | @pytest.mark.parametrize( 283 | "sequence,target,total", 284 | [ 285 | (haem.DNASequence(), haem.DNABase.ADENINE, 0), 286 | (haem.DNASequence("AGCG"), haem.DNABase.GUANINE, 2), 287 | (haem.DNASequence("AGCG"), "G", 2), 288 | ], 289 | ) 290 | def test_count( 291 | sequence: haem.DNASequence, target: typing.Union[haem.DNABase, str], total: int 292 | ) -> None: 293 | assert sequence.count(target) == total 294 | 295 | 296 | @pytest.mark.parametrize( 297 | "sequence,target,total", 298 | [ 299 | (haem.DNASequence(), haem.DNASequence("GA"), 0), 300 | (haem.DNASequence("GATC"), haem.DNASequence("AT"), 1), 301 | (haem.DNASequence("GAGA"), haem.DNASequence("GA"), 2), 302 | (haem.DNASequence(), "GA", 0), 303 | (haem.DNASequence("GATC"), "AT", 1), 304 | (haem.DNASequence("ATAT"), "AT", 2), 305 | (haem.DNASequence(), haem.DNASequence(), 0), 306 | (haem.DNASequence("GATC"), haem.DNASequence(), 0), 307 | (haem.DNASequence(), "", 0), 308 | (haem.DNASequence("GATC"), "", 0), 309 | ], 310 | ) 311 | def test_count_sequence( 312 | sequence: haem.DNASequence, 313 | target: typing.Union[haem.DNASequence, str], 314 | total: int, 315 | ) -> None: 316 | assert sequence.count(target) == total 317 | 318 | 319 | @pytest.mark.parametrize( 320 | "sequence,target,total", 321 | [ 322 | (haem.DNASequence("GAAA"), haem.DNASequence("AA"), 2), 323 | (haem.DNASequence("GAAG"), haem.DNASequence("AA"), 1), 324 | ], 325 | ) 326 | def test_count_overlap( 327 | sequence: haem.DNASequence, 328 | target: typing.Union[haem.DNASequence, str], 329 | total: int, 330 | ) -> None: 331 | assert sequence.count(target, overlap=True) == total 332 | 333 | 334 | @pytest.mark.parametrize( 335 | "sequence,target,result", 336 | [ 337 | (haem.DNASequence(), haem.DNASequence(), None), 338 | (haem.DNASequence(), haem.DNABase("A"), None), 339 | (haem.DNASequence(), "", None), 340 | (haem.DNASequence(), "AT", None), 341 | (haem.DNASequence("ATG"), haem.DNASequence(), None), 342 | (haem.DNASequence("ATG"), haem.DNABase("C"), None), 343 | (haem.DNASequence("ATG"), "", None), 344 | (haem.DNASequence("ATG"), "GT", None), 345 | (haem.DNASequence("ATG"), haem.DNASequence("TG"), 1), 346 | (haem.DNASequence("ATG"), haem.DNABase("G"), 2), 347 | (haem.DNASequence("ATG"), "TG", 1), 348 | ], 349 | ) 350 | def test_find( 351 | sequence: haem.DNASequence, 352 | target: typing.Union[haem.DNASequence, haem.DNABase, str], 353 | result: typing.Optional[int], 354 | ) -> None: 355 | assert sequence.find(target) == result 356 | -------------------------------------------------------------------------------- /tests/test_rna_base.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import typing 3 | 4 | import pytest 5 | 6 | import haem 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "code,base", 11 | [ 12 | ("A", haem.RNABase.ADENINE), 13 | ("C", haem.RNABase.CYTOSINE), 14 | ("G", haem.RNABase.GUANINE), 15 | ("U", haem.RNABase.URACIL), 16 | ("M", haem.RNABase.ADENINE_CYTOSINE), 17 | ("R", haem.RNABase.ADENINE_GUANINE), 18 | ("W", haem.RNABase.ADENINE_URACIL), 19 | ("S", haem.RNABase.CYTOSINE_GUANINE), 20 | ("Y", haem.RNABase.CYTOSINE_URACIL), 21 | ("K", haem.RNABase.GUANINE_URACIL), 22 | ("V", haem.RNABase.ADENINE_CYTOSINE_GUANINE), 23 | ("H", haem.RNABase.ADENINE_CYTOSINE_URACIL), 24 | ("D", haem.RNABase.ADENINE_GUANINE_URACIL), 25 | ("B", haem.RNABase.CYTOSINE_GUANINE_URACIL), 26 | ("N", haem.RNABase.ANY), 27 | (".", haem.RNABase.GAP), 28 | ("-", haem.RNABase.GAP), 29 | ], 30 | ) 31 | def test__new__(code: str, base: haem.RNABase) -> None: 32 | assert haem.RNABase(code) == base 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "code,message", 37 | [("X", 'invalid IUPAC RNA code "X"'), ("XX", "expected a string of length 1")], 38 | ) 39 | def test__new__invalid_code(code: str, message: str) -> None: 40 | with pytest.raises(ValueError) as excinfo: 41 | haem.RNABase(code) 42 | 43 | assert str(excinfo.value) == message 44 | 45 | 46 | def test__repr__() -> None: 47 | assert repr(haem.RNABase.ADENINE) == "RNABase.ADENINE" 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "base,text", 52 | [ 53 | (haem.RNABase.ADENINE, "adenine"), 54 | (haem.RNABase.CYTOSINE, "cytosine"), 55 | (haem.RNABase.GUANINE, "guanine"), 56 | (haem.RNABase.URACIL, "uracil"), 57 | (haem.RNABase.ADENINE_CYTOSINE, "adenine/cytosine"), 58 | (haem.RNABase.ADENINE_GUANINE, "adenine/guanine"), 59 | (haem.RNABase.ADENINE_URACIL, "adenine/uracil"), 60 | (haem.RNABase.CYTOSINE_GUANINE, "cytosine/guanine"), 61 | (haem.RNABase.CYTOSINE_URACIL, "cytosine/uracil"), 62 | (haem.RNABase.GUANINE_URACIL, "guanine/uracil"), 63 | (haem.RNABase.ADENINE_CYTOSINE_GUANINE, "adenine/cytosine/guanine"), 64 | (haem.RNABase.ADENINE_CYTOSINE_URACIL, "adenine/cytosine/uracil"), 65 | (haem.RNABase.ADENINE_GUANINE_URACIL, "adenine/guanine/uracil"), 66 | (haem.RNABase.CYTOSINE_GUANINE_URACIL, "cytosine/guanine/uracil"), 67 | (haem.RNABase.ANY, "any"), 68 | (haem.RNABase.GAP, "gap"), 69 | ], 70 | ) 71 | def test__str__(base: haem.RNABase, text: str) -> None: 72 | assert str(base) == text 73 | 74 | 75 | @pytest.mark.parametrize( 76 | "base,code", 77 | [ 78 | (haem.RNABase.ADENINE, "A"), 79 | (haem.RNABase.CYTOSINE, "C"), 80 | (haem.RNABase.GUANINE, "G"), 81 | (haem.RNABase.URACIL, "U"), 82 | (haem.RNABase.ADENINE_CYTOSINE, "M"), 83 | (haem.RNABase.ADENINE_GUANINE, "R"), 84 | (haem.RNABase.ADENINE_URACIL, "W"), 85 | (haem.RNABase.CYTOSINE_GUANINE, "S"), 86 | (haem.RNABase.CYTOSINE_URACIL, "Y"), 87 | (haem.RNABase.GUANINE_URACIL, "K"), 88 | (haem.RNABase.ADENINE_CYTOSINE_GUANINE, "V"), 89 | (haem.RNABase.ADENINE_CYTOSINE_URACIL, "H"), 90 | (haem.RNABase.ADENINE_GUANINE_URACIL, "D"), 91 | (haem.RNABase.CYTOSINE_GUANINE_URACIL, "B"), 92 | (haem.RNABase.ANY, "N"), 93 | (haem.RNABase.GAP, "-"), 94 | ], 95 | ) 96 | def test_code(base: haem.RNABase, code: str) -> None: 97 | assert base.code == code 98 | 99 | 100 | @pytest.mark.parametrize( 101 | "base,complement", 102 | [ 103 | (haem.RNABase.ADENINE, haem.RNABase.URACIL), 104 | (haem.RNABase.CYTOSINE, haem.RNABase.GUANINE), 105 | (haem.RNABase.GUANINE, haem.RNABase.CYTOSINE), 106 | (haem.RNABase.URACIL, haem.RNABase.ADENINE), 107 | (haem.RNABase.ADENINE_CYTOSINE, haem.RNABase.GUANINE_URACIL), 108 | (haem.RNABase.ADENINE_GUANINE, haem.RNABase.CYTOSINE_URACIL), 109 | (haem.RNABase.ADENINE_URACIL, haem.RNABase.ADENINE_URACIL), 110 | (haem.RNABase.CYTOSINE_GUANINE, haem.RNABase.CYTOSINE_GUANINE), 111 | (haem.RNABase.CYTOSINE_URACIL, haem.RNABase.ADENINE_GUANINE), 112 | (haem.RNABase.GUANINE_URACIL, haem.RNABase.ADENINE_CYTOSINE), 113 | (haem.RNABase.ADENINE_CYTOSINE_GUANINE, haem.RNABase.CYTOSINE_GUANINE_URACIL), 114 | (haem.RNABase.ADENINE_CYTOSINE_URACIL, haem.RNABase.ADENINE_GUANINE_URACIL), 115 | (haem.RNABase.ADENINE_GUANINE_URACIL, haem.RNABase.ADENINE_CYTOSINE_URACIL), 116 | (haem.RNABase.CYTOSINE_GUANINE_URACIL, haem.RNABase.ADENINE_CYTOSINE_GUANINE), 117 | (haem.RNABase.ANY, haem.RNABase.ANY), 118 | (haem.RNABase.GAP, haem.RNABase.GAP), 119 | ], 120 | ) 121 | def test_complement(base: haem.RNABase, complement: haem.RNABase) -> None: 122 | assert base.complement == complement 123 | assert ~base == complement 124 | 125 | 126 | @pytest.mark.parametrize( 127 | "rna_base,dna_base", 128 | [ 129 | (haem.RNABase.ADENINE, haem.DNABase.ADENINE), 130 | (haem.RNABase.CYTOSINE, haem.DNABase.CYTOSINE), 131 | (haem.RNABase.GUANINE, haem.DNABase.GUANINE), 132 | (haem.RNABase.URACIL, haem.DNABase.THYMINE), 133 | (haem.RNABase.ADENINE_CYTOSINE, haem.DNABase.ADENINE_CYTOSINE), 134 | (haem.RNABase.ADENINE_GUANINE, haem.DNABase.ADENINE_GUANINE), 135 | (haem.RNABase.ADENINE_URACIL, haem.DNABase.ADENINE_THYMINE), 136 | (haem.RNABase.CYTOSINE_GUANINE, haem.DNABase.CYTOSINE_GUANINE), 137 | (haem.RNABase.CYTOSINE_URACIL, haem.DNABase.CYTOSINE_THYMINE), 138 | (haem.RNABase.GUANINE_URACIL, haem.DNABase.GUANINE_THYMINE), 139 | (haem.RNABase.ADENINE_CYTOSINE_GUANINE, haem.DNABase.ADENINE_CYTOSINE_GUANINE), 140 | (haem.RNABase.ADENINE_CYTOSINE_URACIL, haem.DNABase.ADENINE_CYTOSINE_THYMINE), 141 | (haem.RNABase.ADENINE_GUANINE_URACIL, haem.DNABase.ADENINE_GUANINE_THYMINE), 142 | (haem.RNABase.CYTOSINE_GUANINE_URACIL, haem.DNABase.CYTOSINE_GUANINE_THYMINE), 143 | (haem.RNABase.ANY, haem.DNABase.ANY), 144 | (haem.RNABase.GAP, haem.DNABase.GAP), 145 | ], 146 | ) 147 | def test_retro_transcribe(rna_base: haem.RNABase, dna_base: haem.DNABase) -> None: 148 | assert rna_base.retro_transcribe() == dna_base 149 | 150 | 151 | def test__eq__() -> None: 152 | assert haem.RNABase.ADENINE == haem.RNABase.ADENINE 153 | 154 | 155 | def test__ne__() -> None: 156 | assert haem.RNABase.ADENINE != haem.RNABase.CYTOSINE 157 | 158 | 159 | @pytest.mark.parametrize("op", [operator.gt, operator.ge, operator.lt, operator.le]) 160 | def test_unsupported_comparison( 161 | op: typing.Callable[[haem.RNABase, haem.RNABase], bool], 162 | ) -> None: 163 | with pytest.raises(TypeError): 164 | op(haem.RNABase.ADENINE, haem.RNABase.CYTOSINE) 165 | 166 | 167 | @pytest.mark.parametrize( 168 | "base,result", [(haem.RNABase.ADENINE, True), (haem.RNABase.GAP, False)] 169 | ) 170 | def test__bool__(base: haem.RNABase, result: bool) -> None: 171 | assert bool(base) == result 172 | 173 | 174 | @pytest.mark.parametrize( 175 | "left,right,result", 176 | [ 177 | (haem.RNABase.ADENINE, haem.RNABase("-"), haem.RNASequence("A-")), 178 | (haem.RNABase.ADENINE, haem.RNASequence("CUU"), haem.RNASequence("ACUU")), 179 | (haem.RNABase.ADENINE, haem.RNASequence(), haem.RNASequence("A")), 180 | (haem.RNABase.ADENINE, "-", haem.RNASequence("A-")), 181 | (haem.RNABase.ADENINE, "", haem.RNASequence("A")), 182 | (haem.RNABase.ADENINE, iter(["A", "U"]), haem.RNASequence("AAU")), 183 | (haem.RNABase.ADENINE, ["A", "A"], haem.RNASequence("AAA")), 184 | ( 185 | haem.RNABase.ADENINE, 186 | [haem.RNABase.ADENINE, haem.RNABase.URACIL], 187 | haem.RNASequence("AAU"), 188 | ), 189 | ( 190 | haem.RNABase.ADENINE, 191 | iter([haem.RNABase.ADENINE, haem.RNABase.URACIL]), 192 | haem.RNASequence("AAU"), 193 | ), 194 | ], 195 | ) 196 | def test__add__( 197 | left: haem.RNABase, 198 | right: typing.Union[ 199 | haem.RNABase, haem.RNASequence, typing.Iterator[str], typing.Sequence[str], str 200 | ], 201 | result: haem.RNASequence, 202 | ) -> None: 203 | assert left + right == result 204 | 205 | 206 | @pytest.mark.parametrize( 207 | "left,right,result", 208 | [ 209 | (haem.RNASequence("CUU"), haem.RNABase("A"), haem.RNASequence("CUUA")), 210 | (haem.RNASequence(), haem.RNABase("A"), haem.RNASequence("A")), 211 | ("C", haem.RNABase("A"), haem.RNASequence("CA")), 212 | ("", haem.RNABase("A"), haem.RNASequence("A")), 213 | (iter(["A", "U"]), haem.RNABase.ADENINE, haem.RNASequence("AUA")), 214 | (["A", "A"], haem.RNABase.ADENINE, haem.RNASequence("AAA")), 215 | ( 216 | [haem.RNABase.ADENINE, haem.RNABase.URACIL], 217 | haem.RNABase.ADENINE, 218 | haem.RNASequence("AUA"), 219 | ), 220 | ( 221 | iter([haem.RNABase.ADENINE, haem.RNABase.URACIL]), 222 | haem.RNABase.ADENINE, 223 | haem.RNASequence("AUA"), 224 | ), 225 | ], 226 | ) 227 | def test__radd__( 228 | left: typing.Union[ 229 | haem.RNASequence, str, typing.Iterator[str], typing.Sequence[str] 230 | ], 231 | right: haem.RNABase, 232 | result: haem.RNASequence, 233 | ) -> None: 234 | assert left + right == result 235 | -------------------------------------------------------------------------------- /tests/test_rna_sequence.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import typing 3 | 4 | import pytest 5 | 6 | import haem 7 | 8 | 9 | def test__new__str() -> None: 10 | assert haem.RNASequence("ACGU") == haem.RNASequence( 11 | [ 12 | haem.RNABase.ADENINE, 13 | haem.RNABase.CYTOSINE, 14 | haem.RNABase.GUANINE, 15 | haem.RNABase.URACIL, 16 | ] 17 | ) 18 | 19 | 20 | def test__new__str__invalid() -> None: 21 | with pytest.raises(ValueError) as excinfo: 22 | haem.RNASequence("ACGUX") 23 | 24 | assert str(excinfo.value) == 'invalid IUPAC RNA code "X"' 25 | 26 | 27 | def test__new__iterable_base() -> None: 28 | assert haem.RNASequence( 29 | iter( 30 | [ 31 | haem.RNABase.ADENINE, 32 | haem.RNABase.CYTOSINE, 33 | haem.RNABase.GUANINE, 34 | haem.RNABase.URACIL, 35 | ] 36 | ) 37 | ) == haem.RNASequence("ACGU") 38 | 39 | 40 | def test__new__iterable_str() -> None: 41 | assert haem.RNASequence(iter(["A", "C", "G", "U"])) == haem.RNASequence("ACGU") 42 | 43 | 44 | def test__new__iterable_invalid() -> None: 45 | with pytest.raises(ValueError) as excinfo: 46 | haem.RNASequence(iter(["A", "C", "G", "X"])) 47 | 48 | assert str(excinfo.value) == 'invalid IUPAC RNA code "X"' 49 | 50 | 51 | def test__new__sequence_bases() -> None: 52 | assert haem.RNASequence( 53 | [ 54 | haem.RNABase.ADENINE, 55 | haem.RNABase.CYTOSINE, 56 | haem.RNABase.GUANINE, 57 | haem.RNABase.URACIL, 58 | ] 59 | ) == haem.RNASequence("ACGU") 60 | 61 | 62 | def test__new__sequence_str() -> None: 63 | assert haem.RNASequence(["A", "C", "G", "U"]) == haem.RNASequence("ACGU") 64 | 65 | 66 | @pytest.mark.parametrize( 67 | "sequence,complement", 68 | [ 69 | (haem.RNASequence(), haem.RNASequence()), 70 | (haem.RNASequence("A"), haem.RNASequence("U")), 71 | (haem.RNASequence("ACGU"), haem.RNASequence("UGCA")), 72 | ], 73 | ) 74 | def test_complement(sequence: haem.RNASequence, complement: haem.RNASequence) -> None: 75 | assert sequence.complement == complement 76 | assert ~sequence == complement 77 | 78 | 79 | @pytest.mark.parametrize( 80 | "rna_sequence,dna_sequence", 81 | [ 82 | (haem.RNASequence(), haem.DNASequence("")), 83 | (haem.RNASequence("ACGU"), haem.DNASequence("ACGT")), 84 | ], 85 | ) 86 | def test_retro_transcribe( 87 | rna_sequence: haem.RNASequence, dna_sequence: haem.DNASequence 88 | ) -> None: 89 | assert rna_sequence.retro_transcribe() == dna_sequence 90 | 91 | 92 | @pytest.mark.parametrize( 93 | "bases,text", 94 | [ 95 | ([], ""), 96 | ([haem.RNABase.ADENINE], ""), 97 | ([haem.RNABase.ADENINE, haem.RNABase.ADENINE_CYTOSINE], ""), 98 | ([haem.RNABase.ADENINE for _ in range(100)], f""), 99 | ], 100 | ) 101 | def test__repr__(bases: typing.List[haem.RNABase], text: str) -> None: 102 | assert repr(haem.RNASequence(bases)) == text 103 | 104 | 105 | @pytest.mark.parametrize( 106 | "bases,text", 107 | [ 108 | ([], ""), 109 | ([haem.RNABase.ADENINE], "A"), 110 | ([haem.RNABase.ADENINE, haem.RNABase.ADENINE_CYTOSINE], "AM"), 111 | ([haem.RNABase.ADENINE for _ in range(20)], "A" * 20), 112 | ( 113 | [haem.RNABase.ADENINE for _ in range(21)] + [haem.RNABase.GUANINE], 114 | f"{'A' * 10}...{'A' * 9}G", 115 | ), 116 | ], 117 | ) 118 | def test__str__(bases: typing.List[haem.RNABase], text: str) -> None: 119 | assert str(haem.RNASequence(bases)) == text 120 | 121 | 122 | @pytest.mark.parametrize("bases", [[], [haem.RNABase.ADENINE, haem.RNABase.GUANINE]]) 123 | def test__eq__(bases: typing.List[haem.RNABase]) -> None: 124 | assert haem.RNASequence(bases) == haem.RNASequence(bases) 125 | 126 | 127 | def test__ne__() -> None: 128 | assert haem.RNASequence( 129 | [haem.RNABase.ADENINE, haem.RNABase.GUANINE] 130 | ) != haem.RNASequence([haem.RNABase.ADENINE, haem.RNABase.URACIL]) 131 | 132 | 133 | @pytest.mark.parametrize("op", [operator.gt, operator.ge, operator.lt, operator.le]) 134 | def test_unsupported_comparison( 135 | op: typing.Callable[[haem.RNASequence, haem.RNASequence], bool], 136 | ) -> None: 137 | with pytest.raises(TypeError): 138 | op(haem.RNASequence(), haem.RNASequence()) 139 | 140 | 141 | @pytest.mark.parametrize("bases,result", [([], False), ([haem.RNABase.ADENINE], True)]) 142 | def test__bool__(bases: typing.List[haem.RNABase], result: bool) -> None: 143 | assert bool(haem.RNASequence(bases)) is result 144 | 145 | 146 | @pytest.mark.parametrize( 147 | "left,right,result", 148 | [ 149 | (haem.RNASequence("A-"), haem.RNABase.GUANINE, haem.RNASequence("A-G")), 150 | (haem.RNASequence("A-"), haem.RNASequence("CUU"), haem.RNASequence("A-CUU")), 151 | (haem.RNASequence("A-"), haem.RNASequence(), haem.RNASequence("A-")), 152 | (haem.RNASequence(), haem.RNASequence(), haem.RNASequence()), 153 | (haem.RNASequence(), haem.RNABase.GUANINE, haem.RNASequence("G")), 154 | (haem.RNASequence("A-"), "", haem.RNASequence("A-")), 155 | (haem.RNASequence("A-"), "GU", haem.RNASequence("A-GU")), 156 | (haem.RNASequence(), "", haem.RNASequence()), 157 | (haem.RNASequence(), "UG", haem.RNASequence("UG")), 158 | ], 159 | ) 160 | def test__add__( 161 | left: haem.RNASequence, 162 | right: typing.Union[haem.RNABase, haem.RNASequence, str], 163 | result: haem.RNASequence, 164 | ) -> None: 165 | assert left + right == result 166 | 167 | 168 | @pytest.mark.parametrize( 169 | "left,right,result", 170 | [ 171 | (haem.RNABase.GUANINE, haem.RNASequence("A-"), haem.RNASequence("GA-")), 172 | (haem.RNABase.GUANINE, haem.RNASequence(), haem.RNASequence("G")), 173 | ("", haem.RNASequence("A-"), haem.RNASequence("A-")), 174 | ("GU", haem.RNASequence("A-"), haem.RNASequence("GUA-")), 175 | ("", haem.RNASequence(), haem.RNASequence()), 176 | ("UG", haem.RNASequence(), haem.RNASequence("UG")), 177 | ], 178 | ) 179 | def test__radd__( 180 | left: typing.Union[haem.RNABase, str], 181 | right: haem.RNASequence, 182 | result: haem.RNASequence, 183 | ) -> None: 184 | assert left + right == result 185 | 186 | 187 | @pytest.mark.parametrize( 188 | "sequence,target,result", 189 | [ 190 | (haem.RNASequence("A"), haem.RNABase.ADENINE, True), 191 | (haem.RNASequence(), haem.RNABase.ADENINE, False), 192 | (haem.RNASequence("G"), haem.RNABase.ADENINE, False), 193 | (haem.RNASequence("AGCG"), haem.RNABase.GUANINE, True), 194 | (haem.RNASequence("AGCG"), haem.RNABase.URACIL, False), 195 | (haem.RNASequence(), haem.RNASequence(), True), 196 | (haem.RNASequence("AGU"), haem.RNASequence("GU"), True), 197 | (haem.RNASequence("AGU"), haem.RNASequence("UG"), False), 198 | (haem.RNASequence("A"), haem.RNASequence("AA"), False), 199 | (haem.RNASequence(), "", True), 200 | (haem.RNASequence("AGU"), "GU", True), 201 | (haem.RNASequence("AGU"), "UG", False), 202 | (haem.RNASequence("A"), "AA", False), 203 | ], 204 | ) 205 | def test__contains__( 206 | sequence: haem.RNASequence, 207 | target: typing.Union[haem.RNABase, haem.RNASequence], 208 | result: bool, 209 | ) -> None: 210 | assert (target in sequence) is result 211 | 212 | 213 | @pytest.mark.parametrize("bases,length", [([], 0), ([haem.RNABase.ADENINE], 1)]) 214 | def test__len__(bases: typing.List[haem.RNABase], length: int) -> None: 215 | assert len(haem.RNASequence(bases)) == length 216 | 217 | 218 | @pytest.mark.parametrize( 219 | "sequence,index,base", 220 | [ 221 | (haem.RNASequence("GAU"), 0, haem.RNABase.GUANINE), 222 | (haem.RNASequence("GAU"), 1, haem.RNABase.ADENINE), 223 | (haem.RNASequence("GAU"), 2, haem.RNABase.URACIL), 224 | (haem.RNASequence("GAU"), -1, haem.RNABase.URACIL), 225 | (haem.RNASequence("GAU"), -2, haem.RNABase.ADENINE), 226 | (haem.RNASequence("GAU"), -3, haem.RNABase.GUANINE), 227 | ], 228 | ) 229 | def test__getitem__index( 230 | sequence: haem.RNASequence, index: int, base: haem.RNABase 231 | ) -> None: 232 | assert sequence[index] == base 233 | 234 | 235 | @pytest.mark.parametrize("index", [3, -4]) 236 | def test__getitem__index_out_of_range(index: int) -> None: 237 | with pytest.raises(IndexError) as excinfo: 238 | haem.RNASequence("GAU")[index] 239 | 240 | assert str(excinfo.value) == "RNASequence index out of range" 241 | 242 | 243 | @pytest.mark.parametrize( 244 | "sequence,slic,result", 245 | [ 246 | (haem.RNASequence(), slice(0, 0), haem.RNASequence()), 247 | (haem.RNASequence("GAU"), slice(0, 2), haem.RNASequence("GA")), 248 | (haem.RNASequence("GAU"), slice(1, 3), haem.RNASequence("AU")), 249 | (haem.RNASequence("GAU"), slice(0, 3), haem.RNASequence("GAU")), 250 | (haem.RNASequence("GAU"), slice(0, 4), haem.RNASequence("GAU")), 251 | (haem.RNASequence("GAU"), slice(0, None), haem.RNASequence("GAU")), 252 | (haem.RNASequence("GAU"), slice(1, None), haem.RNASequence("AU")), 253 | (haem.RNASequence("GAU"), slice(-1, None), haem.RNASequence("U")), 254 | (haem.RNASequence("GAU"), slice(-3, None), haem.RNASequence("GAU")), 255 | (haem.RNASequence("GAU"), slice(-4, None), haem.RNASequence("GAU")), 256 | (haem.RNASequence("GAU"), slice(0, -1), haem.RNASequence("GA")), 257 | (haem.RNASequence("GAUCCA"), slice(0, -1, 2), haem.RNASequence("GUC")), 258 | (haem.RNASequence("GAUCCA"), slice(5, None, -1), haem.RNASequence("ACCUAG")), 259 | (haem.RNASequence("GAUCCA"), slice(None, None, -1), haem.RNASequence("ACCUAG")), 260 | (haem.RNASequence("GAUCCA"), slice(10, 2, -2), haem.RNASequence("AC")), 261 | ], 262 | ) 263 | def test__getitem__slice( 264 | sequence: haem.RNASequence, slic: slice, result: haem.RNASequence 265 | ) -> None: 266 | assert sequence[slic.start : slic.stop : slic.step] == result 267 | 268 | 269 | @pytest.mark.parametrize( 270 | "bases", [[haem.RNABase("U")], [haem.RNABase("U"), haem.RNABase("C")], []] 271 | ) 272 | def test__iter__(bases: typing.List[haem.RNABase]) -> None: 273 | sequence_iter = iter(haem.RNASequence(bases)) 274 | 275 | for base in bases: 276 | assert next(sequence_iter) == base 277 | 278 | with pytest.raises(StopIteration): 279 | next(sequence_iter) 280 | 281 | 282 | @pytest.mark.parametrize( 283 | "sequence,target,total", 284 | [ 285 | (haem.RNASequence(), haem.RNABase.ADENINE, 0), 286 | (haem.RNASequence("AGCG"), haem.RNABase.GUANINE, 2), 287 | (haem.RNASequence("AGCG"), "G", 2), 288 | ], 289 | ) 290 | def test_count( 291 | sequence: haem.RNASequence, target: typing.Union[haem.RNABase, str], total: int 292 | ) -> None: 293 | assert sequence.count(target) == total 294 | 295 | 296 | @pytest.mark.parametrize( 297 | "sequence,target,total", 298 | [ 299 | (haem.RNASequence(), haem.RNASequence("GA"), 0), 300 | (haem.RNASequence("GAUC"), haem.RNASequence("AU"), 1), 301 | (haem.RNASequence("GAGA"), haem.RNASequence("GA"), 2), 302 | (haem.RNASequence(), "GA", 0), 303 | (haem.RNASequence("GAUC"), "AU", 1), 304 | (haem.RNASequence("AUAU"), "AU", 2), 305 | (haem.RNASequence(), haem.RNASequence(), 0), 306 | (haem.RNASequence("GUC"), haem.RNASequence(), 0), 307 | (haem.RNASequence(), "", 0), 308 | (haem.RNASequence("GAUC"), "", 0), 309 | ], 310 | ) 311 | def test_count_sequence( 312 | sequence: haem.RNASequence, 313 | target: typing.Union[haem.RNASequence, str], 314 | total: int, 315 | ) -> None: 316 | assert sequence.count(target) == total 317 | 318 | 319 | @pytest.mark.parametrize( 320 | "sequence,target,total", 321 | [ 322 | (haem.RNASequence("GAAA"), haem.RNASequence("AA"), 2), 323 | (haem.RNASequence("GAAG"), haem.RNASequence("AA"), 1), 324 | ], 325 | ) 326 | def test_count_overlap( 327 | sequence: haem.RNASequence, 328 | target: typing.Union[haem.RNASequence, str], 329 | total: int, 330 | ) -> None: 331 | assert sequence.count(target, overlap=True) == total 332 | 333 | 334 | @pytest.mark.parametrize( 335 | "sequence,target,result", 336 | [ 337 | (haem.RNASequence(), haem.RNASequence(), None), 338 | (haem.RNASequence(), haem.RNABase("A"), None), 339 | (haem.RNASequence(), "", None), 340 | (haem.RNASequence(), "AU", None), 341 | (haem.RNASequence("AUG"), haem.RNASequence(), None), 342 | (haem.RNASequence("AUG"), haem.RNABase("C"), None), 343 | (haem.RNASequence("AUG"), "", None), 344 | (haem.RNASequence("AUG"), "GU", None), 345 | (haem.RNASequence("AUG"), haem.RNASequence("UG"), 1), 346 | (haem.RNASequence("AUG"), haem.RNABase("G"), 2), 347 | (haem.RNASequence("AUG"), "UG", 1), 348 | ], 349 | ) 350 | def test_find( 351 | sequence: haem.RNASequence, 352 | target: typing.Union[haem.RNASequence, haem.RNABase, str], 353 | result: typing.Optional[int], 354 | ) -> None: 355 | assert sequence.find(target) == result 356 | 357 | 358 | @pytest.mark.parametrize( 359 | "rna_sequence,amino_acid_sequence", 360 | [ 361 | (haem.RNASequence("AUGUAA"), haem.AminoAcidSequence("M")), 362 | (haem.RNASequence("AUGUAAA"), haem.AminoAcidSequence("M")), 363 | (haem.RNASequence("CAUGUAA"), haem.AminoAcidSequence("M")), 364 | (haem.RNASequence("AUGUAAAUG"), haem.AminoAcidSequence("M")), 365 | (haem.RNASequence("AUGUGGUAA"), haem.AminoAcidSequence("MW")), 366 | ], 367 | ) 368 | def test_translate( 369 | rna_sequence: haem.RNASequence, amino_acid_sequence: haem.AminoAcidSequence 370 | ) -> None: 371 | assert rna_sequence.translate() == amino_acid_sequence 372 | 373 | 374 | def test_translate_no_start_codon() -> None: 375 | with pytest.raises(ValueError) as excinfo: 376 | haem.RNASequence().translate() 377 | 378 | assert str(excinfo.value) == "no start codon found" 379 | 380 | 381 | def test_translate_no_stop_codon() -> None: 382 | with pytest.raises(ValueError) as excinfo: 383 | haem.RNASequence("AUG").translate() 384 | 385 | assert str(excinfo.value) == "no stop codon found" 386 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.9" 4 | 5 | [[package]] 6 | name = "colorama" 7 | version = "0.4.6" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "exceptiongroup" 16 | version = "1.3.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 24 | ] 25 | 26 | [[package]] 27 | name = "haem" 28 | version = "0.1.0" 29 | source = { editable = "." } 30 | 31 | [package.optional-dependencies] 32 | dev = [ 33 | { name = "maturin" }, 34 | { name = "mypy" }, 35 | { name = "pytest" }, 36 | { name = "ruff" }, 37 | ] 38 | 39 | [package.metadata] 40 | requires-dist = [ 41 | { name = "maturin", marker = "extra == 'dev'", specifier = ">=1.8" }, 42 | { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14" }, 43 | { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" }, 44 | { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9" }, 45 | ] 46 | provides-extras = ["dev"] 47 | 48 | [[package]] 49 | name = "iniconfig" 50 | version = "2.1.0" 51 | source = { registry = "https://pypi.org/simple" } 52 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 53 | wheels = [ 54 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 55 | ] 56 | 57 | [[package]] 58 | name = "maturin" 59 | version = "1.9.4" 60 | source = { registry = "https://pypi.org/simple" } 61 | dependencies = [ 62 | { name = "tomli", marker = "python_full_version < '3.11'" }, 63 | ] 64 | sdist = { url = "https://files.pythonhosted.org/packages/13/7c/b11b870fc4fd84de2099906314ce45488ae17be32ff5493519a6cddc518a/maturin-1.9.4.tar.gz", hash = "sha256:235163a0c99bc6f380fb8786c04fd14dcf6cd622ff295ea3de525015e6ac40cf", size = 213647, upload-time = "2025-08-27T11:37:57.079Z" } 65 | wheels = [ 66 | { url = "https://files.pythonhosted.org/packages/f2/90/0d99389eea1939116fca841cad0763600c8d3183a02a9478d066736c60e8/maturin-1.9.4-py3-none-linux_armv6l.whl", hash = "sha256:6ff37578e3f5fdbe685110d45f60af1f5a7dfce70a1e26dfe3810af66853ecae", size = 8276133, upload-time = "2025-08-27T11:37:23.325Z" }, 67 | { url = "https://files.pythonhosted.org/packages/f4/ed/c8ec68b383e50f084bf1fa9605e62a90cd32a3f75d9894ed3a6e5d4cc5b3/maturin-1.9.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f3837bb53611b2dafa1c090436c330f2d743ba305ef00d8801a371f4495e7e1b", size = 15994496, upload-time = "2025-08-27T11:37:27.092Z" }, 68 | { url = "https://files.pythonhosted.org/packages/84/4e/401ff5f3cfc6b123364d4b94379bf910d7baee32c9c95b72784ff2329357/maturin-1.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4227d627d8e3bfe45877a8d65e9d8351a9d01434549f0da75d2c06a1b570de58", size = 8362228, upload-time = "2025-08-27T11:37:31.181Z" }, 69 | { url = "https://files.pythonhosted.org/packages/51/8e/c56176dd360da9650c62b8a5ecfb85432cf011e97e46c186901e6996002e/maturin-1.9.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:1bb2aa0fa29032e9c5aac03ac400396ddea12cadef242f8967e9c8ef715313a1", size = 8271397, upload-time = "2025-08-27T11:37:33.672Z" }, 70 | { url = "https://files.pythonhosted.org/packages/d2/46/001fcc5c6ad509874896418d6169a61acd619df5b724f99766308c44a99f/maturin-1.9.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:a0868d52934c8a5d1411b42367633fdb5cd5515bec47a534192282167448ec30", size = 8775625, upload-time = "2025-08-27T11:37:35.86Z" }, 71 | { url = "https://files.pythonhosted.org/packages/b4/2e/26fa7574f01c19b7a74680fd70e5bae2e8c40fed9683d1752e765062cc2b/maturin-1.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:68b7b833b25741c0f553b78e8b9e095b31ae7c6611533b3c7b71f84c2cb8fc44", size = 8051117, upload-time = "2025-08-27T11:37:38.278Z" }, 72 | { url = "https://files.pythonhosted.org/packages/73/ee/ca7308832d4f5b521c1aa176d9265f6f93e0bd1ad82a90fd9cd799f6b28c/maturin-1.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:08dc86312afee55af778af919818632e35d8d0464ccd79cb86700d9ea560ccd7", size = 8132122, upload-time = "2025-08-27T11:37:40.499Z" }, 73 | { url = "https://files.pythonhosted.org/packages/45/e8/c623955da75e801a06942edf1fdc4e772a9e8fbc1ceebbdc85d59584dc10/maturin-1.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ef20ffdd943078c4c3699c29fb2ed722bb6b4419efdade6642d1dbf248f94a70", size = 10586762, upload-time = "2025-08-27T11:37:42.718Z" }, 74 | { url = "https://files.pythonhosted.org/packages/3c/4b/19ad558fdf54e151b1b4916ed45f1952ada96684ee6db64f9cd91cabec09/maturin-1.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:368e958468431dfeec80f75eea9639b4356d8c42428b0128444424b083fecfb0", size = 8926988, upload-time = "2025-08-27T11:37:45.492Z" }, 75 | { url = "https://files.pythonhosted.org/packages/7e/27/153ad15eccae26921e8a01812da9f3b7f9013368f8f92c36853f2043b2a3/maturin-1.9.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:273f879214f63f79bfe851cd7d541f8150bdbfae5dfdc3c0c4d125d02d1f41b4", size = 8536758, upload-time = "2025-08-27T11:37:48.213Z" }, 76 | { url = "https://files.pythonhosted.org/packages/43/e3/f304c3bdc3fba9adebe5348d4d2dd015f1152c0a9027aaf52cae0bb182c8/maturin-1.9.4-py3-none-win32.whl", hash = "sha256:ed2e54d132ace7e61829bd49709331007dd9a2cc78937f598aa76a4f69b6804d", size = 7265200, upload-time = "2025-08-27T11:37:50.881Z" }, 77 | { url = "https://files.pythonhosted.org/packages/14/14/f86d0124bf1816b99005c058a1dbdca7cb5850d9cf4b09dcae07a1bc6201/maturin-1.9.4-py3-none-win_amd64.whl", hash = "sha256:8e450bb2c9afdf38a0059ee2e1ec2b17323f152b59c16f33eb9c74edaf1f9f79", size = 8237391, upload-time = "2025-08-27T11:37:53.23Z" }, 78 | { url = "https://files.pythonhosted.org/packages/3f/25/8320fc2591e45b750c3ae71fa596b47aefa802d07d6abaaa719034a85160/maturin-1.9.4-py3-none-win_arm64.whl", hash = "sha256:7a6f980a9b67a5c13c844c268eabd855b54a6a765df4b4bb07d15a990572a4c9", size = 6988277, upload-time = "2025-08-27T11:37:55.429Z" }, 79 | ] 80 | 81 | [[package]] 82 | name = "mypy" 83 | version = "1.18.2" 84 | source = { registry = "https://pypi.org/simple" } 85 | dependencies = [ 86 | { name = "mypy-extensions" }, 87 | { name = "pathspec" }, 88 | { name = "tomli", marker = "python_full_version < '3.11'" }, 89 | { name = "typing-extensions" }, 90 | ] 91 | sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, 94 | { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, 95 | { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, 96 | { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, 97 | { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, 98 | { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, 99 | { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, 100 | { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, 101 | { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, 102 | { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, 103 | { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, 104 | { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, 105 | { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, 106 | { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, 107 | { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, 108 | { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, 109 | { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, 110 | { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, 111 | { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, 112 | { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, 113 | { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, 114 | { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, 115 | { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, 116 | { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, 117 | { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, 118 | { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, 119 | { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, 120 | { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, 121 | { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, 122 | { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, 123 | { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" }, 124 | { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" }, 125 | { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" }, 126 | { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" }, 127 | { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" }, 128 | { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" }, 129 | { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, 130 | ] 131 | 132 | [[package]] 133 | name = "mypy-extensions" 134 | version = "1.1.0" 135 | source = { registry = "https://pypi.org/simple" } 136 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 137 | wheels = [ 138 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 139 | ] 140 | 141 | [[package]] 142 | name = "packaging" 143 | version = "25.0" 144 | source = { registry = "https://pypi.org/simple" } 145 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 146 | wheels = [ 147 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 148 | ] 149 | 150 | [[package]] 151 | name = "pathspec" 152 | version = "0.12.1" 153 | source = { registry = "https://pypi.org/simple" } 154 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 155 | wheels = [ 156 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 157 | ] 158 | 159 | [[package]] 160 | name = "pluggy" 161 | version = "1.6.0" 162 | source = { registry = "https://pypi.org/simple" } 163 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 164 | wheels = [ 165 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 166 | ] 167 | 168 | [[package]] 169 | name = "pygments" 170 | version = "2.19.2" 171 | source = { registry = "https://pypi.org/simple" } 172 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 173 | wheels = [ 174 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 175 | ] 176 | 177 | [[package]] 178 | name = "pytest" 179 | version = "8.4.2" 180 | source = { registry = "https://pypi.org/simple" } 181 | dependencies = [ 182 | { name = "colorama", marker = "sys_platform == 'win32'" }, 183 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 184 | { name = "iniconfig" }, 185 | { name = "packaging" }, 186 | { name = "pluggy" }, 187 | { name = "pygments" }, 188 | { name = "tomli", marker = "python_full_version < '3.11'" }, 189 | ] 190 | sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } 191 | wheels = [ 192 | { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, 193 | ] 194 | 195 | [[package]] 196 | name = "ruff" 197 | version = "0.13.2" 198 | source = { registry = "https://pypi.org/simple" } 199 | sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } 200 | wheels = [ 201 | { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, 202 | { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, 203 | { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, 204 | { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, 205 | { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, 206 | { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, 207 | { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, 208 | { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, 209 | { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, 210 | { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, 211 | { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, 212 | { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, 213 | { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, 214 | { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, 215 | { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, 216 | { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, 217 | { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, 218 | { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, 219 | ] 220 | 221 | [[package]] 222 | name = "tomli" 223 | version = "2.2.1" 224 | source = { registry = "https://pypi.org/simple" } 225 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 226 | wheels = [ 227 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 228 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 229 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 230 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 231 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 232 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 233 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 234 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 235 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 236 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 237 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 238 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 239 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 240 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 241 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 242 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 243 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 244 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 245 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 246 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 247 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 248 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 249 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 250 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 251 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 252 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 253 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 254 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 255 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 256 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 257 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 258 | ] 259 | 260 | [[package]] 261 | name = "typing-extensions" 262 | version = "4.15.0" 263 | source = { registry = "https://pypi.org/simple" } 264 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 265 | wheels = [ 266 | { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 267 | ] 268 | --------------------------------------------------------------------------------