fastasma 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Pérez Rodríguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastasma
3
+ Version: 0.1.1
4
+ Summary: A collection of tools for working with FASTA files.
5
+ Author-email: Daniel Pérez-Rodríguez <daniel.perez.rodriguez@uvigo.es>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Daniel Pérez Rodríguez
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE.txt
30
+ Dynamic: license-file
31
+
32
+ # fastasma
33
+
34
+ A collection of tools for working with FASTA files.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install fastasma
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ import fastasma
46
+
47
+ # Import a FASTA file
48
+ source = fstools.ImportFasta("sequences.fasta")
49
+
50
+ # Annotate sequences
51
+ annotated = fstools.AddAnnotationToHeader(
52
+ source,
53
+ annotation_key="organism",
54
+ separator="_",
55
+ position="suffix"
56
+ )
57
+
58
+ # Write output
59
+ fstools.WriteFasta(annotated, output_path="output.fasta")
60
+ ```
61
+
62
+ ### Pipeline example
63
+
64
+ ```python
65
+ import fastasma
66
+
67
+ # Multiple steps can be chained
68
+ source = fstools.ImportFasta("input.fasta")
69
+ source = fstools.DropAnnotationKeys(source, keys_to_remove=["length"])
70
+ source = fstools.AddAnnotationToHeader(source, annotation_key="organism", position="suffix")
71
+ source = fstools.Head(source, n=10)
72
+ fstools.WriteFasta(source, output_path="output.fasta")
73
+ ```
74
+
75
+ ### Available modules
76
+
77
+ **Importers:** `ImportFasta`, `ImportFastas`, `ImportTSV`
78
+
79
+ **Annotators:** `DropAnnotations`, `DropAnnotationKeys`, `AddTaxonomyFromFilename`, `AddTaxidFromName`, `AddNameFromTaxid`, `AddTaxonomicRankFromTaxid`, `AddAnnotationToHeader`
80
+
81
+ **Mutators:** `Head`, `Tail`, `Sample`
82
+
83
+ **Writers:** `WriteFasta`, `WriteTSV`, `WriteDB`
@@ -0,0 +1,52 @@
1
+ # fastasma
2
+
3
+ A collection of tools for working with FASTA files.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install fastasma
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import fastasma
15
+
16
+ # Import a FASTA file
17
+ source = fstools.ImportFasta("sequences.fasta")
18
+
19
+ # Annotate sequences
20
+ annotated = fstools.AddAnnotationToHeader(
21
+ source,
22
+ annotation_key="organism",
23
+ separator="_",
24
+ position="suffix"
25
+ )
26
+
27
+ # Write output
28
+ fstools.WriteFasta(annotated, output_path="output.fasta")
29
+ ```
30
+
31
+ ### Pipeline example
32
+
33
+ ```python
34
+ import fastasma
35
+
36
+ # Multiple steps can be chained
37
+ source = fstools.ImportFasta("input.fasta")
38
+ source = fstools.DropAnnotationKeys(source, keys_to_remove=["length"])
39
+ source = fstools.AddAnnotationToHeader(source, annotation_key="organism", position="suffix")
40
+ source = fstools.Head(source, n=10)
41
+ fstools.WriteFasta(source, output_path="output.fasta")
42
+ ```
43
+
44
+ ### Available modules
45
+
46
+ **Importers:** `ImportFasta`, `ImportFastas`, `ImportTSV`
47
+
48
+ **Annotators:** `DropAnnotations`, `DropAnnotationKeys`, `AddTaxonomyFromFilename`, `AddTaxidFromName`, `AddNameFromTaxid`, `AddTaxonomicRankFromTaxid`, `AddAnnotationToHeader`
49
+
50
+ **Mutators:** `Head`, `Tail`, `Sample`
51
+
52
+ **Writers:** `WriteFasta`, `WriteTSV`, `WriteDB`
@@ -0,0 +1,276 @@
1
+ import sqlite3
2
+ from fastasma.Types import Sequence, SequenceSource
3
+ from typing import Iterator, Callable, Optional
4
+
5
+ class DropAnnotations:
6
+ def __init__(self, source: SequenceSource):
7
+ self._source = source
8
+
9
+ def __iter__(self):
10
+ return self.yield_sequence()
11
+
12
+ def yield_sequence(self) -> Iterator[Sequence]:
13
+ for seq in self._source:
14
+ yield Sequence(header=seq.header, sequence=seq.sequence)
15
+
16
+ class DropAnnotationKeys:
17
+ def __init__(self, source: SequenceSource, keys_to_remove: list[str]):
18
+ self._source = source
19
+ self._keys_to_remove = set(keys_to_remove)
20
+
21
+ def __iter__(self):
22
+ return self.yield_sequence()
23
+
24
+ def yield_sequence(self) -> Iterator[Sequence]:
25
+ for seq in self._source:
26
+ new_annotation = {
27
+ key: value
28
+ for key, value in seq.annotation.items()
29
+ if key not in self._keys_to_remove
30
+ }
31
+ yield Sequence(header=seq.header, sequence=seq.sequence, annotation=new_annotation)
32
+
33
+ class AddTaxonomyFromFilename:
34
+ def __init__(self, source: SequenceSource, key: Callable = lambda x: x, header_formatter: Callable = None):
35
+ self._source = source
36
+ self._fun = key
37
+ self._header_formatter = header_formatter or self.default_header_formatter
38
+
39
+ def __iter__(self):
40
+ return self.yield_sequence()
41
+
42
+ def default_header_formatter(self, seq: Sequence, taxon: str) -> str:
43
+ return f"{seq.header} taxon={taxon}"
44
+
45
+ def yield_sequence(self) -> Iterator[Sequence]:
46
+ for seq in self._source:
47
+ taxon = self._fun(getattr(self._source, "filepath", None))
48
+ new_header = self._header_formatter(seq, taxon)
49
+ yield Sequence(new_header, seq.sequence)
50
+
51
+ class AddTaxidFromName:
52
+ def __init__(self, source: SequenceSource, taxonomy_db: str, organims_field: str = "organism"):
53
+ """Adds taxid information to Sequence objects from a taxonomy database.
54
+ taxonomy_db schema (SQLite):
55
+ CREATE TABLE names (taxid INTEGER, name TEXT, unique_name TEXT, name_class TEXT);
56
+ Correspondence between sequence headers and taxids: names.name (scientific name) -> names.taxid
57
+ """
58
+ self._source = source
59
+ self._taxonomy_db = taxonomy_db
60
+ self._organism_field = organims_field
61
+ def __iter__(self):
62
+ return self.yield_sequence()
63
+
64
+ def yield_sequence(self) -> Iterator[Sequence]:
65
+ import sqlite3
66
+
67
+ conn = sqlite3.connect(self._taxonomy_db)
68
+ cursor = conn.cursor()
69
+
70
+ # Loads all names and taxids into a dictionary for quick lookup
71
+ cursor.execute("SELECT name, taxid FROM names")
72
+ taxid_dict = {name: taxid for name, taxid in cursor.fetchall()}
73
+
74
+ for seq in self._source:
75
+ organism_name = seq.annotation.get(self._organism_field, None)
76
+ taxid = taxid_dict.get(organism_name)
77
+ new_annotation = dict(seq.annotation)
78
+ new_annotation["taxid"] = taxid
79
+ yield Sequence(header=seq.header, sequence=seq.sequence, annotation=new_annotation)
80
+
81
+ conn.close()
82
+
83
+ class AddNameFromTaxid:
84
+ def __init__(self, source: SequenceSource, taxonomy_db: str, taxid_field: str = "taxid", name_field: str = "organism"):
85
+ """Converts taxid information in Sequence objects to scientific names using a taxonomy database.
86
+ taxonomy_db schema (SQLite):
87
+ CREATE TABLE names (taxid INTEGER, name TEXT, unique_name TEXT, name_class TEXT);
88
+ Correspondence between taxids and scientific names: names.taxid -> names.name (scientific name)
89
+ """
90
+ self._source = source
91
+ self._taxonomy_db = taxonomy_db
92
+ self._taxid_field = taxid_field
93
+ self._name_field = name_field
94
+
95
+ def __iter__(self):
96
+ return self.yield_sequence()
97
+
98
+ def yield_sequence(self) -> Iterator[Sequence]:
99
+ import sqlite3
100
+
101
+ conn = sqlite3.connect(self._taxonomy_db)
102
+ cursor = conn.cursor()
103
+
104
+ # Loads all taxids and names into a dictionary for quick lookup
105
+ cursor.execute("SELECT taxid, name FROM names")
106
+ name_dict = {taxid: name for taxid, name in cursor.fetchall()}
107
+
108
+ for seq in self._source:
109
+ taxid = seq.annotation.get(self._taxid_field, None)
110
+ name = name_dict.get(int(taxid))
111
+ new_annotation = dict(seq.annotation)
112
+ new_annotation[self._name_field] = name
113
+ yield Sequence(header=seq.header, sequence=seq.sequence, annotation=new_annotation)
114
+
115
+ conn.close()
116
+
117
+ class AddTaxonomicRankFromTaxid:
118
+ def __init__(
119
+ self,
120
+ source: SequenceSource,
121
+ taxonomy_db: str,
122
+ taxid_field: str = "taxid",
123
+ rank: str = "species"
124
+ ):
125
+ """Adds taxonomic rank information to Sequence objects from a taxonomy database.
126
+
127
+ Args:
128
+ source: Source that yields Sequence objects
129
+ taxonomy_db: Path to SQLite taxonomy database
130
+ taxid_field: Name of the field containing the taxid in annotations
131
+ rank: Taxonomic rank to retrieve (e.g., 'species', 'genus', 'family')
132
+
133
+ Database schema (SQLite):
134
+ CREATE TABLE nodes (taxid INTEGER PRIMARY KEY, parent INTEGER, rank TEXT);
135
+ CREATE TABLE names (taxid INTEGER, name TEXT, unique_name TEXT, name_class TEXT);
136
+ """
137
+ self._source = source
138
+ self._taxonomy_db = taxonomy_db
139
+ self._taxid_field = taxid_field
140
+ self._rank = rank
141
+
142
+ # Connection will be opened lazily
143
+ self._conn: Optional[sqlite3.Connection] = None
144
+ self._nodes_dict: dict[int, tuple[Optional[int], str]] = {}
145
+ self._names_cache: dict[int, str] = {}
146
+
147
+ def _ensure_connection(self):
148
+ """Lazy initialization of database connection and data loading."""
149
+ if self._conn is not None:
150
+ return
151
+
152
+ self._conn = sqlite3.connect(self._taxonomy_db)
153
+ self._cursor = self._conn.cursor()
154
+
155
+ # Preload nodes into memory for fast traversal
156
+ self._cursor.execute("SELECT taxid, parent, rank FROM nodes")
157
+ self._nodes_dict = {
158
+ taxid: (parent, rank)
159
+ for taxid, parent, rank in self._cursor.fetchall()
160
+ }
161
+
162
+ # Preload scientific names into memory
163
+ self._cursor.execute(
164
+ "SELECT taxid, name FROM names WHERE name_class = 'scientific name'"
165
+ )
166
+ self._names_cache = {
167
+ taxid: name
168
+ for taxid, name in self._cursor.fetchall()
169
+ }
170
+
171
+ def _get_rank_name(self, taxid: int) -> Optional[str]:
172
+ """Traverse taxonomy tree to find the requested rank and return its name.
173
+
174
+ Args:
175
+ taxid: Starting taxonomic ID
176
+
177
+ Returns:
178
+ Scientific name of the taxon at the requested rank, or None if not found
179
+ """
180
+ current_taxid: Optional[int] = taxid
181
+ max_iterations = 1000 # Prevent infinite loops in corrupted data
182
+ iterations = 0
183
+
184
+ while current_taxid is not None and iterations < max_iterations:
185
+ node_info = self._nodes_dict.get(current_taxid)
186
+ if node_info is None:
187
+ # Taxid not found in database
188
+ break
189
+
190
+ parent, rank = node_info
191
+
192
+ if rank == self._rank:
193
+ # Found the requested rank, return the scientific name
194
+ return self._names_cache.get(current_taxid)
195
+
196
+ current_taxid = parent
197
+ iterations += 1
198
+
199
+ return None
200
+
201
+ def __enter__(self):
202
+ """Context manager entry."""
203
+ self._ensure_connection()
204
+ return self
205
+
206
+ def __exit__(self, exc_type, exc_val, exc_tb):
207
+ """Context manager exit - ensures connection is closed."""
208
+ self.close()
209
+ return False
210
+
211
+ def close(self):
212
+ """Close database connection."""
213
+ if self._conn is not None:
214
+ self._conn.close()
215
+ self._conn = None
216
+
217
+ def __iter__(self):
218
+ return self.yield_sequence()
219
+
220
+ def yield_sequence(self) -> Iterator[Sequence]:
221
+ """Yield sequences with added taxonomic rank information.
222
+
223
+ Yields:
224
+ Sequence objects with the taxonomic rank added to annotations
225
+ """
226
+ self._ensure_connection()
227
+
228
+ try:
229
+ for seq in self._source:
230
+ rank_name = None
231
+
232
+ # Get taxid from annotations
233
+ taxid_str = seq.annotation.get(self._taxid_field) if seq.annotation else None
234
+
235
+ if taxid_str is not None:
236
+ try:
237
+ taxid = int(taxid_str)
238
+ rank_name = self._get_rank_name(taxid)
239
+ except (ValueError, TypeError):
240
+ # Invalid taxid format, leave rank_name as None
241
+ pass
242
+
243
+ # Create new annotation dict with the rank added
244
+ new_annotation = dict(seq.annotation) if seq.annotation else {}
245
+ new_annotation[self._rank] = rank_name
246
+
247
+ yield Sequence(
248
+ header=seq.header,
249
+ sequence=seq.sequence,
250
+ annotation=new_annotation
251
+ )
252
+ finally:
253
+ # Ensure connection is closed when iteration completes or fails
254
+ self.close()
255
+
256
+ class AddAnnotationToHeader:
257
+ def __init__(self, source: SequenceSource, annotation_key: str, separator: str = "_", position: str = "suffix"):
258
+ self._source = source
259
+ self._annotation_key = annotation_key
260
+ self._separator = separator
261
+ self._position = position
262
+
263
+ def __iter__(self):
264
+ return self.yield_sequence()
265
+
266
+ def yield_sequence(self) -> Iterator[Sequence]:
267
+ for seq in self._source:
268
+ value = seq.annotation.get(self._annotation_key) if seq.annotation else None
269
+ if value is not None:
270
+ if self._position == "prefix":
271
+ new_header = f"{value}{self._separator}{seq.header}"
272
+ else:
273
+ new_header = f"{seq.header}{self._separator}{value}"
274
+ else:
275
+ new_header = seq.header
276
+ yield Sequence(header=new_header, sequence=seq.sequence, annotation=seq.annotation)
@@ -0,0 +1,128 @@
1
+ import os
2
+ import re
3
+ from typing import List, Iterator
4
+ from fastasma.Types import Sequence
5
+
6
+
7
+ class ImportFasta:
8
+ def __init__(self, filepath: str):
9
+ self.filepath = filepath
10
+ self._valid_seq = re.compile("^[A-Za-z*-]+$")
11
+
12
+ def __iter__(self):
13
+ return self.yield_sequence()
14
+
15
+ def _is_header(self, text: str)-> bool:
16
+ return bool(text) and text[0] == ">"
17
+
18
+ def _parse_header(self, line: str)-> str:
19
+ # Returns the name of the sequence without the annotations
20
+ if not line.startswith(">"):
21
+ raise ValueError(f"Invalid FASTA header: {line}")
22
+ return line[1:].split()[0]
23
+
24
+ def _parse_sequence(self, lines: List[str])-> str:
25
+ seq = ''.join(lines).replace(" ", "").replace("\n", "")
26
+ if not self._valid_seq.match(seq):
27
+ raise ValueError(f"Invalid FASTA sequence: {seq}")
28
+ return seq
29
+
30
+ def _parse_annotation(self, line: str) -> dict:
31
+ # Parses annotations in the header line formatted as [key=value] or key=value
32
+ # considering that brackets may contain spaces
33
+ annotations = {}
34
+ parts = re.findall(r'\[.*?\]|[^ \[\]]+', line[1:])
35
+ for part in parts[1:]: # Skip the first part which is the header
36
+ part = part.strip("[]") # Remove brackets if present
37
+ if '=' in part:
38
+ key, value = part.split('=', 1)
39
+ annotations[key] = value
40
+ return annotations
41
+
42
+ def _yield_sequence(self, filepath: str) -> Iterator[Sequence]:
43
+ header: str | None = None
44
+ seq_lines: list[str] = []
45
+ annotations: dict = {}
46
+ with open(filepath, "r") as file:
47
+ for raw in file:
48
+ line = raw.strip()
49
+ if not line:
50
+ continue
51
+ if self._is_header(line):
52
+ if header is not None:
53
+ yield Sequence(
54
+ header = header,
55
+ sequence = self._parse_sequence(seq_lines),
56
+ annotation=annotations
57
+ )
58
+ header = self._parse_header(line)
59
+ annotations = self._parse_annotation(line)
60
+ seq_lines = []
61
+ else:
62
+ seq_lines.append(line)
63
+ if header is not None:
64
+ yield Sequence(
65
+ header = header,
66
+ sequence = self._parse_sequence(seq_lines),
67
+ annotation=annotations
68
+ )
69
+
70
+ def yield_sequence(self) -> Iterator[Sequence]:
71
+ for seq in self._yield_sequence(self.filepath):
72
+ yield seq
73
+
74
+ class ImportFastas:
75
+ def __init__(self, filepaths: List[str] | None = None, directory: str | None = None):
76
+ if filepaths is None and directory is None:
77
+ raise ValueError("Either filepaths or directory must be provided.")
78
+ if directory is not None:
79
+ filepaths = [
80
+ os.path.join(directory, f)
81
+ for f in os.listdir(directory)
82
+ if f.endswith(".fasta") or f.endswith(".fa")
83
+ ]
84
+ self.filepaths = filepaths
85
+
86
+ def __iter__(self):
87
+ return self.yield_sequence()
88
+
89
+ def yield_sequence(self) -> Iterator[Sequence]:
90
+ for path in self.filepaths:
91
+ fasta = ImportFasta(path)
92
+ self.filepath = path # For use in AddTaxonomyFromFilename
93
+ for seq in fasta.yield_sequence():
94
+ yield seq
95
+
96
+ class ImportTSV:
97
+ def __init__(self, filepath: str, header_idx: int = 0, seq_idx: int = 1, annotation_idx: dict | None = None, header: bool = True):
98
+ """Parses a TSV file to yield Sequence objects.
99
+ Args:
100
+ filepath (str): Path to the TSV file.
101
+ header_idx (int): Column index for the header.
102
+ seq_idx (int): Column index for the sequence.
103
+ annotation_idx (dict | None): Optional dictionary mapping annotation names to column indices.
104
+ header (bool): Whether the TSV file has a header row to skip.
105
+ """
106
+ self.filepath = filepath
107
+ self.header_idx = header_idx
108
+ self.seq_idx = seq_idx
109
+ self.annotation_idx = annotation_idx or {}
110
+ self.header = header
111
+ def __iter__(self):
112
+ return self.yield_sequence()
113
+
114
+ def yield_sequence(self) -> Iterator[Sequence]:
115
+ with open(self.filepath, "r") as file:
116
+ for i, line in enumerate(file):
117
+ if self.header and i == 0:
118
+ continue
119
+ parts = line.strip().split("\t")
120
+ if not parts or len(parts) <= max(self.header_idx, self.seq_idx, *(self.annotation_idx.values())):
121
+ continue
122
+ header = parts[self.header_idx]
123
+ sequence = parts[self.seq_idx]
124
+ annotation = {
125
+ key: parts[idx]
126
+ for key, idx in self.annotation_idx.items()
127
+ }
128
+ yield Sequence(header=header, sequence=sequence, annotation=annotation)
@@ -0,0 +1,59 @@
1
+ from fslab.Types import Sequence, SequenceSource
2
+ from typing import Iterator, Callable, Optional
3
+ from collections import deque
4
+
5
+ class Head:
6
+ def __init__(self, source: SequenceSource, n: int):
7
+ self._source = source
8
+ self._n = n
9
+
10
+ def __iter__(self):
11
+ return self.yield_sequence()
12
+
13
+ def yield_sequence(self) -> Iterator[Sequence]:
14
+ count = 0
15
+ for seq in self._source:
16
+ if count >= self._n:
17
+ break
18
+ yield seq
19
+ count += 1
20
+
21
+ class Tail:
22
+ def __init__(self, source: SequenceSource, n: int):
23
+ self._source = source
24
+ self._n = n
25
+
26
+ def __iter__(self):
27
+ return self.yield_sequence()
28
+
29
+ def yield_sequence(self) -> Iterator[Sequence]:
30
+ buffer = deque(maxlen=self._n)
31
+ for seq in self._source:
32
+ buffer.append(seq)
33
+ for seq in buffer:
34
+ yield seq
35
+
36
+ class Sample:
37
+ def __init__(self, source: SequenceSource, k: int, seed: Optional[int] = None):
38
+ """Randomly samples k sequences from the source without unpacking the generator into memory.
39
+ Uses reservoir sampling algorithm.
40
+ """
41
+ import random
42
+ self._source = source
43
+ self._k = k
44
+ self._random = random.Random(seed)
45
+
46
+ def __iter__(self):
47
+ return self.yield_sequence()
48
+
49
+ def yield_sequence(self) -> Iterator[Sequence]:
50
+ reservoir = []
51
+ for i, seq in enumerate(self._source):
52
+ if i < self._k:
53
+ reservoir.append(seq)
54
+ else:
55
+ j = self._random.randint(0, i)
56
+ if j < self._k:
57
+ reservoir[j] = seq
58
+ for seq in reservoir:
59
+ yield seq
@@ -0,0 +1,13 @@
1
+ from typing import Iterator, Protocol
2
+ from dataclasses import dataclass
3
+
4
+ @dataclass
5
+ class Sequence:
6
+ header: str
7
+ sequence: str
8
+ annotation: dict = None
9
+
10
+ class SequenceSource(Protocol):
11
+ def yield_sequence(self) -> Iterator[Sequence]:
12
+ ...
13
+
@@ -0,0 +1,149 @@
1
+ from fastasma.Types import Sequence, SequenceSource
2
+ from typing import List, Iterator, Callable
3
+
4
+ class SeqSourceToString:
5
+ def __init__(self, source: SequenceSource, wrap: int = 80, annotaton_transform: Callable = None):
6
+ self._source = source
7
+ self._wrap = wrap
8
+
9
+ def __iter__(self):
10
+ return self._sequence_to_str()
11
+
12
+ def annotation_transform(self, seq: Sequence) -> str:
13
+ return ' '.join(f"[{key}={value}]" for key, value in seq.annotation.items()) or ""
14
+
15
+ def _format_header(self, seq: Sequence) -> str:
16
+ annotation = self.annotation_transform(seq)
17
+ if annotation:
18
+ return f">{seq.header} {annotation}\n"
19
+ else:
20
+ return f">{seq.header}\n"
21
+
22
+ def _sequence_to_str(self)-> Iterator[str]:
23
+ for seq in self._source.yield_sequence():
24
+ header_line = self._format_header(seq)
25
+ seq_lines = [
26
+ seq.sequence[i:i+self._wrap] + "\n"
27
+ for i in range(0, len(seq.sequence), self._wrap)
28
+ ]
29
+ yield header_line + ''.join(seq_lines)
30
+
31
+ def yield_sequence(self) -> Iterator[str]:
32
+ return self._sequence_to_str()
33
+
34
+ class WriteFasta:
35
+ def __init__(self, source: SequenceSource, output_path: str, wrap: int = 80):
36
+ self._source = source
37
+ self._output_path = output_path
38
+ self._wrap = wrap
39
+ self.write()
40
+
41
+ def write(self):
42
+ with open(self._output_path, "w") as outfile:
43
+ fasta_str_source = SeqSourceToString(self._source, wrap=self._wrap)
44
+ for record_str in fasta_str_source:
45
+ outfile.write(record_str)
46
+
47
+ class WriteTSV:
48
+ def __init__(self, source: SequenceSource, output_path: str, sep: str = "\t"):
49
+ """Converts Sequence objects to a TSV file, using annotations as columns, plus header and sequence."""
50
+ self._source = source
51
+ self._output_path = output_path
52
+ self._sep = sep
53
+ self.write()
54
+
55
+ def _get_columns(self) -> List[str]:
56
+ columns = set()
57
+ for seq in self._source:
58
+ columns.update(seq.annotation.keys())
59
+ return ["header", "sequence"] + sorted(columns)
60
+
61
+ def write(self):
62
+ columns = self._get_columns()
63
+ with open(self._output_path, "w") as outfile:
64
+ outfile.write(self._sep.join(columns) + "\n")
65
+ for seq in self._source:
66
+ row = [
67
+ seq.header,
68
+ seq.sequence
69
+ ] + [
70
+ str(seq.annotation.get(col, "")) for col in columns[2:]
71
+ ]
72
+ outfile.write(self._sep.join(row) + "\n")
73
+
74
+ class WriteDB:
75
+ def __init__(self, source: SequenceSource, db_path: str):
76
+ """Writes Sequence objects to a SQLite database, using annotations as columns, plus header and sequence.
77
+ All the annotations are used as foreign keys to a separate annotations table. Annotations are stored as follows:
78
+ sequence table:
79
+ CREATE TABLE sequences (sequence_id INTEGER PRIMARY KEY, sequence_header TEXT, sequence_sequence TEXT);
80
+ annotation table:
81
+ CREATE TABLE annotations (annotation_id INTEGER PRIMARY KEY, annotation_name TEXT);
82
+ annotation_data table:
83
+ CREATE TABLE annotation_data (annotation_data_id INTEGER PRIMARY KEY, annotation_id INTEGER, annotation_data_value TEXT, sequence_id INTEGER,
84
+ FOREIGN KEY(annotation_id) REFERENCES annotations(annotation_id),
85
+ FOREIGN KEY(sequence_id) REFERENCES sequences(sequence_id));
86
+ """
87
+ self._source = source
88
+ self._db_path = db_path
89
+ self.write()
90
+
91
+ def write(self):
92
+ import sqlite3
93
+
94
+ conn = sqlite3.connect(self._db_path)
95
+ cursor = conn.cursor()
96
+
97
+ # Create tables
98
+ cursor.execute("""
99
+ CREATE TABLE IF NOT EXISTS sequences (
100
+ sequence_id INTEGER PRIMARY KEY,
101
+ sequence_header TEXT,
102
+ sequence_sequence TEXT
103
+ )
104
+ """)
105
+ cursor.execute("""
106
+ CREATE TABLE IF NOT EXISTS annotations (
107
+ annotation_id INTEGER PRIMARY KEY,
108
+ annotation_name TEXT UNIQUE
109
+ )
110
+ """)
111
+ cursor.execute("""
112
+ CREATE TABLE IF NOT EXISTS annotation_data (
113
+ annotation_data_id INTEGER PRIMARY KEY,
114
+ annotation_id INTEGER,
115
+ annotation_data_value TEXT,
116
+ sequence_id INTEGER,
117
+ FOREIGN KEY(annotation_id) REFERENCES annotations(annotation_id),
118
+ FOREIGN KEY(sequence_id) REFERENCES sequences(sequence_id)
119
+ )
120
+ """)
121
+
122
+ # Insert data
123
+ for seq in self._source:
124
+ cursor.execute(
125
+ "INSERT INTO sequences (sequence_header, sequence_sequence) VALUES (?, ?)",
126
+ (seq.header, seq.sequence)
127
+ )
128
+ sequence_id = cursor.lastrowid
129
+
130
+ for key, value in seq.annotation.items():
131
+ cursor.execute(
132
+ "INSERT OR IGNORE INTO annotations (annotation_name) VALUES (?)",
133
+ (key,)
134
+ )
135
+ cursor.execute(
136
+ "SELECT annotation_id FROM annotations WHERE annotation_name = ?",
137
+ (key,)
138
+ )
139
+ annotation_id = cursor.fetchone()[0]
140
+
141
+ cursor.execute(
142
+ "INSERT INTO annotation_data (annotation_id, annotation_data_value, sequence_id) VALUES (?, ?, ?)",
143
+ (annotation_id, value, sequence_id)
144
+ )
145
+
146
+ conn.commit()
147
+ conn.close()
148
+
149
+
@@ -0,0 +1,4 @@
1
+ from fastasma.Importers import ImportFasta, ImportFastas, ImportTSV
2
+ from fastasma.Annotators import DropAnnotations, DropAnnotationKeys, AddTaxonomyFromFilename, AddTaxidFromName, AddNameFromTaxid, AddTaxonomicRankFromTaxid, AddAnnotationToHeader
3
+ from fastasma.Writers import WriteFasta, WriteTSV, WriteDB
4
+ from fastasma.Mutators import Head, Tail, Sample
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastasma
3
+ Version: 0.1.1
4
+ Summary: A collection of tools for working with FASTA files.
5
+ Author-email: Daniel Pérez-Rodríguez <daniel.perez.rodriguez@uvigo.es>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Daniel Pérez Rodríguez
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE.txt
30
+ Dynamic: license-file
31
+
32
+ # fastasma
33
+
34
+ A collection of tools for working with FASTA files.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install fastasma
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ import fastasma
46
+
47
+ # Import a FASTA file
48
+ source = fstools.ImportFasta("sequences.fasta")
49
+
50
+ # Annotate sequences
51
+ annotated = fstools.AddAnnotationToHeader(
52
+ source,
53
+ annotation_key="organism",
54
+ separator="_",
55
+ position="suffix"
56
+ )
57
+
58
+ # Write output
59
+ fstools.WriteFasta(annotated, output_path="output.fasta")
60
+ ```
61
+
62
+ ### Pipeline example
63
+
64
+ ```python
65
+ import fastasma
66
+
67
+ # Multiple steps can be chained
68
+ source = fstools.ImportFasta("input.fasta")
69
+ source = fstools.DropAnnotationKeys(source, keys_to_remove=["length"])
70
+ source = fstools.AddAnnotationToHeader(source, annotation_key="organism", position="suffix")
71
+ source = fstools.Head(source, n=10)
72
+ fstools.WriteFasta(source, output_path="output.fasta")
73
+ ```
74
+
75
+ ### Available modules
76
+
77
+ **Importers:** `ImportFasta`, `ImportFastas`, `ImportTSV`
78
+
79
+ **Annotators:** `DropAnnotations`, `DropAnnotationKeys`, `AddTaxonomyFromFilename`, `AddTaxidFromName`, `AddNameFromTaxid`, `AddTaxonomicRankFromTaxid`, `AddAnnotationToHeader`
80
+
81
+ **Mutators:** `Head`, `Tail`, `Sample`
82
+
83
+ **Writers:** `WriteFasta`, `WriteTSV`, `WriteDB`
@@ -0,0 +1,13 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ fastasma/Annotators.py
5
+ fastasma/Importers.py
6
+ fastasma/Mutators.py
7
+ fastasma/Types.py
8
+ fastasma/Writers.py
9
+ fastasma/__init__.py
10
+ fastasma.egg-info/PKG-INFO
11
+ fastasma.egg-info/SOURCES.txt
12
+ fastasma.egg-info/dependency_links.txt
13
+ fastasma.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ fastasma
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "fastasma"
3
+ version = "0.1.1"
4
+ description = "A collection of tools for working with FASTA files."
5
+ authors = [
6
+ { name = "Daniel Pérez-Rodríguez", email = "daniel.perez.rodriguez@uvigo.es" }
7
+ ]
8
+ readme = "README.md"
9
+ license = { file = "LICENSE.txt" }
10
+ requires-python = ">=3.8"
11
+
12
+ [build-system]
13
+ requires = ["setuptools"]
14
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+