dayhoff-tools 1.1.4__tar.gz → 1.1.5__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.
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/PKG-INFO +1 -1
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/fasta.py +144 -41
- dayhoff_tools-1.1.5/dayhoff_tools/intake/gtdb.py +269 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/pyproject.toml +1 -1
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/README.md +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/__init__.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/chemistry/standardizer.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/chemistry/utils.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/cli/__init__.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/cli/cloud_commands.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/cli/main.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/cli/swarm_commands.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/cli/utility_commands.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/base.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/deploy_aws.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/deploy_gcp.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/deploy_utils.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/job_runner.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/processors.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/deployment/swarm.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/embedders.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/file_ops.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/h5.py +0 -0
- {dayhoff_tools-1.1.4/dayhoff_tools → dayhoff_tools-1.1.5/dayhoff_tools/intake}/gcp.py +0 -0
- {dayhoff_tools-1.1.4/dayhoff_tools → dayhoff_tools-1.1.5/dayhoff_tools/intake}/kegg.py +0 -0
- {dayhoff_tools-1.1.4/dayhoff_tools → dayhoff_tools-1.1.5/dayhoff_tools/intake}/mmseqs.py +0 -0
- {dayhoff_tools-1.1.4/dayhoff_tools → dayhoff_tools-1.1.5/dayhoff_tools/intake}/structure.py +0 -0
- {dayhoff_tools-1.1.4/dayhoff_tools → dayhoff_tools-1.1.5/dayhoff_tools/intake}/uniprot.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/logs.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/sqlite.py +0 -0
- {dayhoff_tools-1.1.4 → dayhoff_tools-1.1.5}/dayhoff_tools/warehouse.py +0 -0
@@ -264,65 +264,168 @@ def split_fasta(
|
|
264
264
|
target_folder: str,
|
265
265
|
base_name: str,
|
266
266
|
sequences_per_file: int = 1000,
|
267
|
-
max_files=None,
|
267
|
+
max_files: int | None = None,
|
268
|
+
show_progress: bool = True,
|
269
|
+
target_chunk_size_bytes: int | None = None,
|
268
270
|
) -> int:
|
269
|
-
"""Split a FASTA file into multiple smaller files within a target folder
|
271
|
+
"""Split a FASTA file into multiple smaller files within a target folder,
|
272
|
+
with an overall progress bar. Files can be split based on a target number
|
273
|
+
of sequences or an approximate target file size in bytes.
|
270
274
|
|
271
275
|
Args:
|
272
276
|
fasta_file (str): Path to the input FASTA file.
|
273
277
|
target_folder (str): Path to the folder where output files will be saved.
|
274
278
|
base_name (str): Used to make output filenames: eg, basename_1.fasta.
|
275
279
|
sequences_per_file (int): Number of sequences per output file.
|
276
|
-
|
280
|
+
This is used if target_chunk_size_bytes is None.
|
281
|
+
max_files (int, optional): Maximum number of files to create.
|
282
|
+
If None, all sequences are processed.
|
283
|
+
show_progress (bool): If True, display a progress bar based on
|
284
|
+
file size processed. Defaults to True.
|
285
|
+
target_chunk_size_bytes (int, optional): Approximate target size for
|
286
|
+
each output file in bytes. If set, this takes precedence over
|
287
|
+
sequences_per_file. The actual file size may be slightly larger to
|
288
|
+
ensure full FASTA entries. Defaults to None.
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
int: The number of output files created.
|
277
292
|
"""
|
278
293
|
# Ensure the target folder exists
|
279
294
|
os.makedirs(target_folder, exist_ok=True)
|
280
295
|
|
281
|
-
#
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
296
|
+
# We create output files lazily (on first sequence) so we don't end up with
|
297
|
+
# spurious empty files. `files_created` tracks the number of *real* files
|
298
|
+
# present on disk when we finish.
|
299
|
+
files_created = 0
|
300
|
+
current_output_file_sequence_count = 0
|
301
|
+
current_output_file_bytes_written = 0
|
302
|
+
pbar: tqdm | None = None
|
303
|
+
output_file = None # Will be opened when we encounter the first header line
|
304
|
+
output_file_path = ""
|
305
|
+
|
306
|
+
if target_chunk_size_bytes is not None:
|
307
|
+
print(
|
308
|
+
f"Splitting by target chunk size: {target_chunk_size_bytes / (1024*1024):.2f} MB"
|
290
309
|
)
|
291
|
-
|
292
|
-
|
293
|
-
for line in fasta:
|
294
|
-
# Check if we've reached the maximum number of files, if specified
|
295
|
-
if max_files is not None and file_count > max_files:
|
296
|
-
break
|
310
|
+
else:
|
311
|
+
print(f"Splitting by sequences per file: {sequences_per_file}")
|
297
312
|
|
298
|
-
|
299
|
-
|
300
|
-
|
313
|
+
try:
|
314
|
+
# Open the large FASTA file for reading
|
315
|
+
with open(fasta_file, "r", buffering=1024 * 1024) as fasta:
|
316
|
+
if show_progress:
|
317
|
+
total_size = os.path.getsize(fasta_file)
|
318
|
+
pbar = tqdm(
|
319
|
+
total=total_size,
|
320
|
+
unit="B",
|
321
|
+
unit_scale=True,
|
322
|
+
desc=f"Splitting {os.path.basename(fasta_file)}",
|
323
|
+
)
|
301
324
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
sequence_count = 1 # Reset sequence count for the new file
|
325
|
+
# We create output files on demand. The very first file is not
|
326
|
+
# opened until we see the first sequence header. This prevents
|
327
|
+
# an empty file from being created when the input FASTA is empty
|
328
|
+
# or when `max_files` is reached before any data are written.
|
329
|
+
def _open_new_output_file():
|
330
|
+
nonlocal output_file, output_file_path, files_created
|
309
331
|
|
310
|
-
|
311
|
-
|
312
|
-
|
332
|
+
files_created += 1
|
333
|
+
output_file_path = os.path.join(
|
334
|
+
target_folder, f"{base_name}_{files_created}.fasta"
|
335
|
+
)
|
336
|
+
output_file = open(output_file_path, "w", buffering=1024 * 1024)
|
313
337
|
|
314
|
-
|
315
|
-
|
338
|
+
# Helper for logging and closing the current file
|
339
|
+
def _close_current_output_file():
|
340
|
+
nonlocal output_file, current_output_file_sequence_count, current_output_file_bytes_written
|
341
|
+
if output_file and not output_file.closed:
|
342
|
+
output_file.close()
|
343
|
+
print(
|
344
|
+
f"File written: {output_file_path} "
|
345
|
+
f"(Sequences: {current_output_file_sequence_count}, "
|
346
|
+
f"Bytes: {current_output_file_bytes_written} / {(current_output_file_bytes_written / (1024*1024)):.2f} MB)"
|
316
347
|
)
|
317
|
-
output_file = open(output_file_path, "w", buffering=1024 * 1024)
|
318
|
-
|
319
|
-
# Write the line to the current output file
|
320
|
-
output_file.write(line)
|
321
348
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
349
|
+
for line in fasta:
|
350
|
+
line_bytes = len(line.encode("utf-8"))
|
351
|
+
if pbar:
|
352
|
+
pbar.update(line_bytes)
|
353
|
+
|
354
|
+
# Note: we don't enforce `max_files` here; we enforce it only when we
|
355
|
+
# are about to create *another* file (see logic further below). This
|
356
|
+
# ensures we finish writing the current file before stopping.
|
357
|
+
|
358
|
+
# If line starts with ">", it's the beginning of a new sequence
|
359
|
+
if line.startswith(">"):
|
360
|
+
# Decide whether we need to roll over to a new output file.
|
361
|
+
needs_new_file = False # reset each time we encounter a header
|
362
|
+
|
363
|
+
if (
|
364
|
+
output_file is not None
|
365
|
+
and current_output_file_sequence_count > 0
|
366
|
+
):
|
367
|
+
if target_chunk_size_bytes is not None:
|
368
|
+
# Size-based splitting takes precedence over sequence count.
|
369
|
+
if (
|
370
|
+
current_output_file_bytes_written
|
371
|
+
>= target_chunk_size_bytes
|
372
|
+
):
|
373
|
+
needs_new_file = True
|
374
|
+
else:
|
375
|
+
# Fallback to sequence-count based splitting.
|
376
|
+
if current_output_file_sequence_count >= sequences_per_file:
|
377
|
+
needs_new_file = True
|
378
|
+
|
379
|
+
if needs_new_file:
|
380
|
+
_close_current_output_file()
|
381
|
+
|
382
|
+
# Respect `max_files`: do not create another file if limit reached
|
383
|
+
if max_files is not None and files_created >= max_files:
|
384
|
+
break
|
385
|
+
|
386
|
+
_open_new_output_file()
|
387
|
+
current_output_file_sequence_count = 0
|
388
|
+
current_output_file_bytes_written = 0
|
389
|
+
|
390
|
+
# Opening first file if not already open
|
391
|
+
if output_file is None:
|
392
|
+
_open_new_output_file()
|
393
|
+
|
394
|
+
current_output_file_sequence_count += 1
|
395
|
+
|
396
|
+
# Write the line to the current output file (which should now exist)
|
397
|
+
if output_file is not None:
|
398
|
+
output_file.write(line)
|
399
|
+
current_output_file_bytes_written += line_bytes
|
400
|
+
|
401
|
+
# After loop, ensure the last file is handled
|
402
|
+
_close_current_output_file()
|
403
|
+
|
404
|
+
finally:
|
405
|
+
if pbar:
|
406
|
+
pbar.close()
|
407
|
+
# Ensure the file is closed in case of an exception before the natural end
|
408
|
+
if output_file and not output_file.closed:
|
409
|
+
output_file.close()
|
410
|
+
# It's hard to know the state to print a meaningful message here if an exception occurred mid-file.
|
411
|
+
# The primary 'File written' messages are handled within the loop and at the end of normal processing.
|
412
|
+
|
413
|
+
# If the last file was empty and removed, and it was the only file, file_count might be 1.
|
414
|
+
# Adjust file_count if the last output file was empty and removed.
|
415
|
+
if os.path.exists(output_file_path) and os.path.getsize(output_file_path) == 0:
|
416
|
+
# This can happen if max_files is hit exactly when a new file is due to be created,
|
417
|
+
# or if the input file itself is empty or contains no FASTA entries after the last split point.
|
418
|
+
# We should not count this empty file if it was removed.
|
419
|
+
# However, file_count is already incremented *before* a new file is opened.
|
420
|
+
# The logic for removing empty files is tricky to perfectly align with file_count
|
421
|
+
# without more complex state tracking. The current return reflects the number of
|
422
|
+
# *attempted* file creations that weren't immediately curtailed by max_files.
|
423
|
+
# For simplicity, we'll return the file_count as is, understanding it might
|
424
|
+
# include an empty file that was subsequently removed if it was the very last one.
|
425
|
+
# A more robust approach might decrement file_count if the last created file path was removed.
|
426
|
+
pass
|
427
|
+
|
428
|
+
return files_created
|
326
429
|
|
327
430
|
|
328
431
|
def subtract_fasta_files(file1: str, file2: str, output_file: str):
|
@@ -0,0 +1,269 @@
|
|
1
|
+
import collections.abc
|
2
|
+
import csv
|
3
|
+
import gzip
|
4
|
+
import pathlib
|
5
|
+
import re
|
6
|
+
|
7
|
+
from tqdm import tqdm
|
8
|
+
|
9
|
+
_ACCESSION_REGEX = re.compile(r"(GC[AF]_[0-9]+\.[0-9]+)")
|
10
|
+
|
11
|
+
|
12
|
+
def _extract_accession_from_filename(filename_str: str) -> str:
|
13
|
+
"""
|
14
|
+
Extracts the genome assembly accession (e.g., GCA_XXXXXXXXX.X or GCF_XXXXXXXXX.X)
|
15
|
+
from a filename.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
filename_str (str): The filename string.
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
str: The extracted accession or "UNKNOWN_ACCESSION" if not found.
|
22
|
+
"""
|
23
|
+
match = _ACCESSION_REGEX.search(filename_str)
|
24
|
+
if match:
|
25
|
+
return match.group(1)
|
26
|
+
return "UNKNOWN_ACCESSION"
|
27
|
+
|
28
|
+
|
29
|
+
def process_gtdb_files_to_fasta(
|
30
|
+
gtdb_top_folder: str,
|
31
|
+
output_fasta_path: str,
|
32
|
+
chunk_size: int = 10000,
|
33
|
+
) -> None:
|
34
|
+
"""
|
35
|
+
Processes a top-level GTDB folder containing gzipped FASTA files (.faa.gz)
|
36
|
+
and combines all protein sequences into a single FASTA file.
|
37
|
+
|
38
|
+
Output is written in chunks for efficiency with large datasets.
|
39
|
+
A progress bar is displayed during processing.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
gtdb_top_folder (str): Path to the top-level GTDB directory.
|
43
|
+
output_fasta_path (str): Path to write the combined FASTA file.
|
44
|
+
chunk_size (int, optional): Number of sequences to process before
|
45
|
+
writing a chunk to the output file. Defaults to 10000.
|
46
|
+
"""
|
47
|
+
gtdb_path = pathlib.Path(gtdb_top_folder)
|
48
|
+
faa_files = list(gtdb_path.rglob("*.faa.gz"))
|
49
|
+
|
50
|
+
if not faa_files:
|
51
|
+
print(f"No .faa.gz files found in {gtdb_top_folder}")
|
52
|
+
return
|
53
|
+
|
54
|
+
fasta_entries_chunk = []
|
55
|
+
sequences_in_current_chunk = 0
|
56
|
+
|
57
|
+
with open(output_fasta_path, "w") as fasta_out_file:
|
58
|
+
current_header_id = None
|
59
|
+
current_sequence_lines = []
|
60
|
+
|
61
|
+
for faa_file_path in tqdm(faa_files, desc="Processing GTDB files to FASTA"):
|
62
|
+
try:
|
63
|
+
with gzip.open(faa_file_path, "rt") as gz_file:
|
64
|
+
for line_content in gz_file:
|
65
|
+
line = line_content.strip()
|
66
|
+
if not line: # Skip empty lines
|
67
|
+
continue
|
68
|
+
if line.startswith(">"):
|
69
|
+
if current_header_id and current_sequence_lines:
|
70
|
+
sequence_string = "".join(current_sequence_lines)
|
71
|
+
fasta_entries_chunk.append(
|
72
|
+
f">{current_header_id}\n{sequence_string}\n"
|
73
|
+
)
|
74
|
+
sequences_in_current_chunk += 1
|
75
|
+
|
76
|
+
# Parse new header
|
77
|
+
header_content = line[1:]
|
78
|
+
parts = header_content.split(None, 1)
|
79
|
+
current_header_id = parts[0]
|
80
|
+
current_sequence_lines = []
|
81
|
+
|
82
|
+
if sequences_in_current_chunk >= chunk_size:
|
83
|
+
if fasta_entries_chunk:
|
84
|
+
fasta_out_file.write("".join(fasta_entries_chunk))
|
85
|
+
fasta_entries_chunk = []
|
86
|
+
sequences_in_current_chunk = 0
|
87
|
+
else:
|
88
|
+
if current_header_id:
|
89
|
+
current_sequence_lines.append(line)
|
90
|
+
|
91
|
+
if current_header_id and current_sequence_lines:
|
92
|
+
sequence_string = "".join(current_sequence_lines)
|
93
|
+
fasta_entries_chunk.append(
|
94
|
+
f">{current_header_id}\n{sequence_string}\n"
|
95
|
+
)
|
96
|
+
sequences_in_current_chunk += 1
|
97
|
+
|
98
|
+
# Reset state for the next file to ensure clean parsing start for that file
|
99
|
+
current_header_id = None
|
100
|
+
current_sequence_lines = []
|
101
|
+
|
102
|
+
except gzip.BadGzipFile:
|
103
|
+
tqdm.write(
|
104
|
+
f"Warning: Skipping corrupted or non-gzipped file: {faa_file_path}"
|
105
|
+
)
|
106
|
+
current_header_id = None
|
107
|
+
current_sequence_lines = []
|
108
|
+
except Exception as e:
|
109
|
+
tqdm.write(f"Warning: Error processing file {faa_file_path}: {e}")
|
110
|
+
current_header_id = None
|
111
|
+
current_sequence_lines = []
|
112
|
+
|
113
|
+
if fasta_entries_chunk:
|
114
|
+
fasta_out_file.write("".join(fasta_entries_chunk))
|
115
|
+
|
116
|
+
print(f"Processing complete. Output FASTA file created: {output_fasta_path}")
|
117
|
+
|
118
|
+
|
119
|
+
def process_gtdb_files_to_csv(
|
120
|
+
gtdb_top_folder: str,
|
121
|
+
output_csv_path: str,
|
122
|
+
chunk_size: int = 10000,
|
123
|
+
) -> None:
|
124
|
+
"""
|
125
|
+
Processes a top-level GTDB folder containing gzipped FASTA files (.faa.gz)
|
126
|
+
and creates a CSV file with detailed information for each sequence entry.
|
127
|
+
|
128
|
+
The CSV includes the genome assembly accession, original FASTA header ID,
|
129
|
+
and header description for each entry. Output is written in chunks for
|
130
|
+
efficiency with large datasets. A progress bar is displayed during processing.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
gtdb_top_folder (str): Path to the top-level GTDB directory.
|
134
|
+
output_csv_path (str): Path to write the CSV file.
|
135
|
+
chunk_size (int, optional): Number of sequences to process before
|
136
|
+
writing a chunk to the output file. Defaults to 10000.
|
137
|
+
"""
|
138
|
+
gtdb_path = pathlib.Path(gtdb_top_folder)
|
139
|
+
faa_files = list(gtdb_path.rglob("*.faa.gz"))
|
140
|
+
|
141
|
+
if not faa_files:
|
142
|
+
print(f"No .faa.gz files found in {gtdb_top_folder}")
|
143
|
+
return
|
144
|
+
|
145
|
+
def _serial_iter(paths):
|
146
|
+
"""Yield the same structure as the parallel branch but serially."""
|
147
|
+
for p in paths:
|
148
|
+
row_generator_for_file, file_warnings = _csv_rows_from_single_faa(str(p))
|
149
|
+
yield row_generator_for_file, file_warnings
|
150
|
+
|
151
|
+
# Open output CSV for streaming writes.
|
152
|
+
with open(output_csv_path, "w", newline="") as csv_out_file:
|
153
|
+
csv_writer = csv.writer(csv_out_file)
|
154
|
+
csv_writer.writerow(
|
155
|
+
[
|
156
|
+
"genome_assembly_accession",
|
157
|
+
"original_fasta_header_id",
|
158
|
+
"original_fasta_header_description",
|
159
|
+
]
|
160
|
+
)
|
161
|
+
|
162
|
+
rows_buffer: list[list[str]] = []
|
163
|
+
|
164
|
+
# Choose the iterator depending on workers.
|
165
|
+
result_iter = _serial_iter(faa_files)
|
166
|
+
progress_iter = tqdm(
|
167
|
+
result_iter, total=len(faa_files), desc="Processing GTDB files to CSV"
|
168
|
+
)
|
169
|
+
|
170
|
+
# Consume iterator and stream rows to disk in chunks.
|
171
|
+
for row_generator_for_file, file_warnings in progress_iter:
|
172
|
+
# Add rows to buffer and flush in chunk-size batches.
|
173
|
+
# This will consume the generator, and in doing so, populate file_warnings if errors occur.
|
174
|
+
for r in row_generator_for_file:
|
175
|
+
rows_buffer.append(r)
|
176
|
+
if len(rows_buffer) >= chunk_size:
|
177
|
+
csv_writer.writerows(rows_buffer)
|
178
|
+
rows_buffer.clear()
|
179
|
+
|
180
|
+
# Now that the generator for the file has been processed (or attempted),
|
181
|
+
# emit any warnings that were collected for this specific file.
|
182
|
+
for w in file_warnings:
|
183
|
+
tqdm.write(w)
|
184
|
+
|
185
|
+
# Flush remaining rows.
|
186
|
+
if rows_buffer:
|
187
|
+
csv_writer.writerows(rows_buffer)
|
188
|
+
|
189
|
+
print(f"Processing complete. Output CSV file created: {output_csv_path}")
|
190
|
+
|
191
|
+
|
192
|
+
# ---------------------------------------------------------------------------
|
193
|
+
# Helper functions (private)
|
194
|
+
# ---------------------------------------------------------------------------
|
195
|
+
|
196
|
+
|
197
|
+
def _csv_rows_from_single_faa(
|
198
|
+
faa_file_path: str,
|
199
|
+
) -> tuple[collections.abc.Iterable[list[str]], list[str]]:
|
200
|
+
"""Parse a single gzipped FASTA (`.faa.gz`) file into CSV rows.
|
201
|
+
|
202
|
+
Parameters
|
203
|
+
----------
|
204
|
+
faa_file_path
|
205
|
+
Path (as ``str``) to the ``.faa.gz`` file.
|
206
|
+
|
207
|
+
Returns
|
208
|
+
-------
|
209
|
+
tuple[collections.abc.Iterable[list[str]], list[str]]
|
210
|
+
* First element – an iterable (generator) of CSV rows ``[accession, header_id, description]``.
|
211
|
+
* Second element – list of warning strings produced while processing
|
212
|
+
the file. The caller is responsible for emitting them.
|
213
|
+
"""
|
214
|
+
warnings: list[str] = [] # Outer scope warnings list
|
215
|
+
faa_path = pathlib.Path(faa_file_path)
|
216
|
+
current_file_accession = _extract_accession_from_filename(faa_path.name)
|
217
|
+
|
218
|
+
def _generate_rows_iter_inner() -> (
|
219
|
+
collections.abc.Iterable[list[str]]
|
220
|
+
): # Renamed for clarity
|
221
|
+
# Local parsing state for the generator
|
222
|
+
current_header_id_gen = None
|
223
|
+
current_header_desc_gen = ""
|
224
|
+
has_sequence_lines_gen = False
|
225
|
+
|
226
|
+
try:
|
227
|
+
with gzip.open(faa_file_path, "rt") as gz_file:
|
228
|
+
for line_content in gz_file:
|
229
|
+
line = line_content.strip()
|
230
|
+
if not line:
|
231
|
+
continue
|
232
|
+
if line.startswith(">"):
|
233
|
+
if current_header_id_gen and has_sequence_lines_gen:
|
234
|
+
yield [
|
235
|
+
current_file_accession,
|
236
|
+
current_header_id_gen,
|
237
|
+
current_header_desc_gen,
|
238
|
+
]
|
239
|
+
|
240
|
+
header_content = line[1:]
|
241
|
+
parts = header_content.split(None, 1)
|
242
|
+
current_header_id_gen = parts[0]
|
243
|
+
current_header_desc_gen = parts[1] if len(parts) > 1 else ""
|
244
|
+
has_sequence_lines_gen = False
|
245
|
+
else:
|
246
|
+
if current_header_id_gen:
|
247
|
+
has_sequence_lines_gen = True
|
248
|
+
|
249
|
+
# Add final entry if the file ended after sequence lines.
|
250
|
+
if current_header_id_gen and has_sequence_lines_gen:
|
251
|
+
yield [
|
252
|
+
current_file_accession,
|
253
|
+
current_header_id_gen,
|
254
|
+
current_header_desc_gen,
|
255
|
+
]
|
256
|
+
except gzip.BadGzipFile:
|
257
|
+
# Exception handled inside the generator.
|
258
|
+
# Append to the outer warnings list and terminate generator.
|
259
|
+
warnings.append(
|
260
|
+
f"Warning: Skipping corrupted or non-gzipped file: {faa_file_path}"
|
261
|
+
)
|
262
|
+
return # Stop generation
|
263
|
+
except Exception as exc:
|
264
|
+
warnings.append(f"Warning: Error processing file {faa_file_path}: {exc}")
|
265
|
+
return # Stop generation
|
266
|
+
|
267
|
+
# Directly return the generator instance and the warnings list.
|
268
|
+
# The warnings list will be populated by the generator if errors occur during its execution.
|
269
|
+
return _generate_rows_iter_inner(), warnings
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
5
5
|
|
6
6
|
[project]
|
7
7
|
name = "dayhoff-tools"
|
8
|
-
version = "1.1.
|
8
|
+
version = "1.1.5"
|
9
9
|
description = "Common tools for all the repos at Dayhoff Labs"
|
10
10
|
authors = [
|
11
11
|
{name = "Daniel Martin-Alarcon", email = "dma@dayhofflabs.com"}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|