protpy 1.4.0__tar.gz → 1.4.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.
Files changed (35) hide show
  1. {protpy-1.4.0/protpy.egg-info → protpy-1.4.1}/PKG-INFO +2 -1
  2. {protpy-1.4.0 → protpy-1.4.1}/docs/conf.py +1 -1
  3. {protpy-1.4.0 → protpy-1.4.1}/protpy/__init__.py +2 -2
  4. protpy-1.4.1/protpy/encode.py +624 -0
  5. {protpy-1.4.0 → protpy-1.4.1/protpy.egg-info}/PKG-INFO +2 -1
  6. {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/requires.txt +1 -0
  7. {protpy-1.4.0 → protpy-1.4.1}/pyproject.toml +2 -1
  8. protpy-1.4.1/tests/test_encode.py +947 -0
  9. {protpy-1.4.0 → protpy-1.4.1}/tests/test_protpy.py +2 -2
  10. protpy-1.4.0/protpy/encode.py +0 -203
  11. protpy-1.4.0/tests/test_encode.py +0 -304
  12. {protpy-1.4.0 → protpy-1.4.1}/LICENSE +0 -0
  13. {protpy-1.4.0 → protpy-1.4.1}/MANIFEST.in +0 -0
  14. {protpy-1.4.0 → protpy-1.4.1}/README.md +0 -0
  15. {protpy-1.4.0 → protpy-1.4.1}/protpy/_constants.py +0 -0
  16. {protpy-1.4.0 → protpy-1.4.1}/protpy/autocorrelation.py +0 -0
  17. {protpy-1.4.0 → protpy-1.4.1}/protpy/composition.py +0 -0
  18. {protpy-1.4.0 → protpy-1.4.1}/protpy/conjoint_triad.py +0 -0
  19. {protpy-1.4.0 → protpy-1.4.1}/protpy/ctd.py +0 -0
  20. {protpy-1.4.0 → protpy-1.4.1}/protpy/data/README.md +0 -0
  21. {protpy-1.4.0 → protpy-1.4.1}/protpy/data/grantham-physicochemical-distance-matrix.json +0 -0
  22. {protpy-1.4.0 → protpy-1.4.1}/protpy/data/schneider-wrede-physicochemical-distance-matrix.json +0 -0
  23. {protpy-1.4.0 → protpy-1.4.1}/protpy/sequence_order.py +0 -0
  24. {protpy-1.4.0 → protpy-1.4.1}/protpy/utils.py +0 -0
  25. {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/SOURCES.txt +0 -0
  26. {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/dependency_links.txt +0 -0
  27. {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/top_level.txt +0 -0
  28. {protpy-1.4.0 → protpy-1.4.1}/setup.cfg +0 -0
  29. {protpy-1.4.0 → protpy-1.4.1}/tests/__init__.py +0 -0
  30. {protpy-1.4.0 → protpy-1.4.1}/tests/test_autocorrelation.py +0 -0
  31. {protpy-1.4.0 → protpy-1.4.1}/tests/test_composition.py +0 -0
  32. {protpy-1.4.0 → protpy-1.4.1}/tests/test_conjoint_triad.py +0 -0
  33. {protpy-1.4.0 → protpy-1.4.1}/tests/test_ctd.py +0 -0
  34. {protpy-1.4.0 → protpy-1.4.1}/tests/test_sequence_order.py +0 -0
  35. {protpy-1.4.0 → protpy-1.4.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: protpy
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences.
5
5
  Author-email: AJ McKenna <amckenna41@qub.ac.uk>
6
6
  Maintainer-email: AJ McKenna <amckenna41@qub.ac.uk>
@@ -35,6 +35,7 @@ License-File: LICENSE
35
35
  Requires-Dist: aaindex>=1.2.0
36
36
  Requires-Dist: numpy>=2.4.4
37
37
  Requires-Dist: pandas>=3.0.2
38
+ Requires-Dist: tqdm>=4.0.0
38
39
  Requires-Dist: varname>=0.15.1
39
40
  Provides-Extra: test
40
41
  Requires-Dist: pytest; extra == "test"
@@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('..'))
13
13
  project = 'protpy'
14
14
  copyright = '2026, AJ McKenna'
15
15
  author = 'AJ McKenna'
16
- release = '1.4.0'
16
+ release = '1.4.1'
17
17
 
18
18
  # -- General configuration ---------------------------------------------------
19
19
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@@ -4,7 +4,7 @@ from .conjoint_triad import *
4
4
  from .ctd import *
5
5
  from .sequence_order import *
6
6
  from .utils import get_descriptor_info, list_descriptors, print_descriptor_info
7
- from .encode import all_descriptors
7
+ from .encode import all_descriptors, encode_dataset, read_sequences_from_file
8
8
 
9
9
  #canonical amino acids available in the package
10
10
  amino_acids = ["A", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "P",
@@ -12,7 +12,7 @@ amino_acids = ["A", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "P",
12
12
 
13
13
  #protPy package metadata
14
14
  __name__ = 'protpy'
15
- __version__ = "1.4.0"
15
+ __version__ = "1.4.1"
16
16
  __description__ = "Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences."
17
17
  __author__ = 'AJ McKenna: https://github.com/amckenna41'
18
18
  __authorEmail__ = 'amckenna41@qub.ac.uk'
@@ -0,0 +1,624 @@
1
+ ################################################################################
2
+ ############# Encode ##############
3
+ ################################################################################
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import pandas as pd
9
+ from tqdm import tqdm as _tqdm
10
+
11
+ from .composition import (
12
+ amino_acid_composition,
13
+ dipeptide_composition,
14
+ tripeptide_composition,
15
+ gravy,
16
+ aromaticity,
17
+ instability_index,
18
+ isoelectric_point,
19
+ molecular_weight,
20
+ charge_distribution,
21
+ hydrophobic_polar_charged_composition,
22
+ secondary_structure_propensity,
23
+ kmer_composition,
24
+ reduced_alphabet_composition,
25
+ motif_composition,
26
+ amino_acid_pair_composition,
27
+ aliphatic_index,
28
+ extinction_coefficient,
29
+ boman_index,
30
+ aggregation_propensity,
31
+ hydrophobic_moment,
32
+ shannon_entropy,
33
+ pseudo_amino_acid_composition,
34
+ amphiphilic_pseudo_amino_acid_composition,
35
+ )
36
+ from .autocorrelation import (
37
+ moreaubroto_autocorrelation,
38
+ moran_autocorrelation,
39
+ geary_autocorrelation,
40
+ )
41
+ from .conjoint_triad import conjoint_triad as _conjoint_triad
42
+ from .ctd import ctd_
43
+ from .sequence_order import (
44
+ sequence_order_coupling_number_all,
45
+ quasi_sequence_order_all,
46
+ )
47
+ from ._constants import _validate_sequence
48
+
49
+ #descriptors that accept extra keyword arguments beyond sequence
50
+ _PARAMETERISED_DESCRIPTORS: frozenset[str] = frozenset({
51
+ "charge_distribution",
52
+ "kmer_composition",
53
+ "reduced_alphabet_composition",
54
+ "motif_composition",
55
+ "aggregation_propensity",
56
+ "hydrophobic_moment",
57
+ "pseudo_amino_acid_composition",
58
+ "amphiphilic_pseudo_amino_acid_composition",
59
+ "moreaubroto_autocorrelation",
60
+ "moran_autocorrelation",
61
+ "geary_autocorrelation",
62
+ "ctd_",
63
+ "sequence_order_coupling_number_all",
64
+ "quasi_sequence_order_all",
65
+ })
66
+
67
+
68
+ def all_descriptors(
69
+ sequence: str,
70
+ params: dict[str, dict] | None = None,
71
+ show_progress: bool = False,
72
+ output_file: str | None = "descriptors.csv",
73
+ save_each_sequence: bool = True,
74
+ _show_descriptor_list: bool = True,
75
+ ) -> pd.DataFrame:
76
+ """
77
+ Calculate all available protpy descriptors for a protein sequence and
78
+ return them concatenated into a single DataFrame (1 x N_total).
79
+
80
+ The combined variants of multi-property descriptors are used to avoid
81
+ redundancy: ``ctd_`` is called with ``all_ctd=True`` (covers
82
+ ctd_composition, ctd_transition and ctd_distribution across all 7
83
+ properties), and ``sequence_order_coupling_number_all`` /
84
+ ``quasi_sequence_order_all`` cover both the schneider-wrede and grantham
85
+ distance matrices.
86
+
87
+ Parameters
88
+ ==========
89
+ :sequence: str
90
+ Protein sequence string (case-insensitive, canonical amino acids only).
91
+ Must be longer than 30 residues to satisfy the default lag requirements
92
+ of autocorrelation and sequence-order descriptors (unless overridden
93
+ via ``params``).
94
+
95
+ :params: dict[str, dict] | None (default=None)
96
+ Optional per-descriptor keyword arguments. Keys must be descriptor
97
+ function names; values are dicts of keyword arguments passed directly
98
+ to that descriptor. Unrecognised descriptor names raise a ``ValueError``.
99
+ Descriptors not present in ``params`` use their default parameter values.
100
+
101
+ Only the following descriptors accept extra parameters::
102
+
103
+ charge_distribution → ph
104
+ kmer_composition → k
105
+ reduced_alphabet_composition → alphabet_size
106
+ motif_composition → motifs
107
+ aggregation_propensity → window, hydrophobicity_threshold,
108
+ charge_threshold
109
+ hydrophobic_moment → window, angle
110
+ pseudo_amino_acid_composition → lamda, weight, properties
111
+ amphiphilic_pseudo_amino_acid_composition
112
+ → lamda, weight, properties
113
+ moreaubroto_autocorrelation → lag, properties, normalize
114
+ moran_autocorrelation → lag, properties, normalize
115
+ geary_autocorrelation → lag, properties, normalize
116
+ ctd_ → all_ctd
117
+ sequence_order_coupling_number_all → lag
118
+ quasi_sequence_order_all → lag, weight
119
+
120
+ Example::
121
+
122
+ df = protpy.all_descriptors(seq, params={
123
+ 'charge_distribution': {'ph': 5.5},
124
+ 'kmer_composition': {'k': 3},
125
+ 'pseudo_amino_acid_composition': {'lamda': 10, 'weight': 0.1},
126
+ 'moreaubroto_autocorrelation': {'lag': 15, 'normalize': False},
127
+ 'quasi_sequence_order_all': {'lag': 20, 'weight': 0.2},
128
+ })
129
+
130
+ :show_progress: bool (default=False)
131
+ When ``True``, display a ``tqdm`` progress bar that names the
132
+ descriptor currently being computed and tracks overall progress.
133
+
134
+ :output_file: str | None (default="descriptors.csv")
135
+ Path to the CSV file where results are written. Pass ``None`` to
136
+ disable all file output. When calling this function in a loop over
137
+ multiple sequences the rows are accumulated in the file across calls;
138
+ each call appends one row to any previously saved sequences.
139
+
140
+ :save_each_sequence: bool (default=True)
141
+ When ``True``, the output CSV is updated once after all descriptors
142
+ for the current sequence have been computed, appending a complete
143
+ (no-NaN) row. When calling this function in a loop over multiple
144
+ sequences, each completed sequence is written immediately so that
145
+ progress is preserved if the process is interrupted. When ``False``,
146
+ nothing is written to the CSV within this call. Has no effect when
147
+ ``output_file`` is ``None``.
148
+
149
+ Returns
150
+ =======
151
+ :descriptors_df: pd.DataFrame
152
+ DataFrame of shape 1 x N containing all descriptor values, where N is
153
+ the total number of features across all descriptors.
154
+
155
+ Raises
156
+ ======
157
+ TypeError
158
+ If ``sequence`` is not a string, or if ``params`` is not a dict.
159
+ ValueError
160
+ If ``sequence`` is empty or contains non-canonical amino acid characters,
161
+ or if a key in ``params`` is not a recognised descriptor name.
162
+
163
+ Examples
164
+ ========
165
+ >>> import protpy
166
+ >>> seq = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD"
167
+ >>> df = protpy.all_descriptors(seq)
168
+ >>> df.shape[0]
169
+ 1
170
+ >>> df = protpy.all_descriptors(seq, show_progress=True) # with progress bar
171
+ >>> df = protpy.all_descriptors(seq, output_file="out.csv") # save to file
172
+ >>> df = protpy.all_descriptors(seq, output_file=None) # no file output
173
+ """
174
+ sequence = _validate_sequence(sequence)
175
+
176
+ if params is None:
177
+ params = {}
178
+
179
+ if not isinstance(params, dict):
180
+ raise TypeError(f"params must be a dict, got {type(params)}.")
181
+
182
+ unknown = set(params) - _PARAMETERISED_DESCRIPTORS
183
+ if unknown:
184
+ raise ValueError(
185
+ f"Unknown or non-parameterisable descriptor(s) in params: {sorted(unknown)}. "
186
+ f"Valid keys are: {sorted(_PARAMETERISED_DESCRIPTORS)}."
187
+ )
188
+
189
+ def _p(name: str) -> dict:
190
+ return params.get(name, {})
191
+
192
+ # k=2 duplicates dipeptide_composition; k=3 duplicates tripeptide_composition.
193
+ # Only include kmer_composition when the user requests a k value that is unique.
194
+ _kmer_k = _p("kmer_composition").get("k", 2)
195
+
196
+ steps: list[tuple[str, object]] = [
197
+ # ── Composition ────────────────────────────────────────────────────────
198
+ ("[Composition] Amino acid composition", lambda: amino_acid_composition(sequence)),
199
+ ("[Composition] Dipeptide composition", lambda: dipeptide_composition(sequence)),
200
+ ("[Composition] Tripeptide composition", lambda: tripeptide_composition(sequence)),
201
+ ("[Composition] GRAVY index", lambda: gravy(sequence)),
202
+ ("[Composition] Aromaticity", lambda: aromaticity(sequence)),
203
+ ("[Composition] Instability index", lambda: instability_index(sequence)),
204
+ ("[Composition] Isoelectric point", lambda: isoelectric_point(sequence)),
205
+ ("[Composition] Molecular weight", lambda: molecular_weight(sequence)),
206
+ ("[Composition] Charge distribution", lambda: charge_distribution(sequence, **_p("charge_distribution"))),
207
+ ("[Composition] Hydrophobic / polar / charged comp.", lambda: hydrophobic_polar_charged_composition(sequence)),
208
+ ("[Composition] Secondary structure propensity", lambda: secondary_structure_propensity(sequence)),
209
+ *([("[Composition] K-mer composition", lambda: kmer_composition(sequence, **_p("kmer_composition")))]
210
+ if _kmer_k not in (2, 3) else []),
211
+ ("[Composition] Reduced alphabet composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))),
212
+ ("[Composition] Motif composition", lambda: motif_composition(sequence, **_p("motif_composition"))),
213
+ ("[Composition] Amino acid pair composition", lambda: amino_acid_pair_composition(sequence)),
214
+ ("[Composition] Aliphatic index", lambda: aliphatic_index(sequence)),
215
+ ("[Composition] Extinction coefficient", lambda: extinction_coefficient(sequence)),
216
+ ("[Composition] Boman index", lambda: boman_index(sequence)),
217
+ ("[Composition] Aggregation propensity", lambda: aggregation_propensity(sequence, **_p("aggregation_propensity"))),
218
+ ("[Composition] Hydrophobic moment", lambda: hydrophobic_moment(sequence, **_p("hydrophobic_moment"))),
219
+ ("[Composition] Shannon entropy", lambda: shannon_entropy(sequence)),
220
+ ("[Composition] Pseudo amino acid composition", lambda: pseudo_amino_acid_composition(sequence, **_p("pseudo_amino_acid_composition"))),
221
+ ("[Composition] Amphiphilic pseudo amino acid comp.", lambda: amphiphilic_pseudo_amino_acid_composition(sequence, **_p("amphiphilic_pseudo_amino_acid_composition"))),
222
+ # ── Autocorrelation ────────────────────────────────────────────────────
223
+ ("[Autocorrelation] Moreaubroto autocorrelation", lambda: moreaubroto_autocorrelation(sequence, **_p("moreaubroto_autocorrelation"))),
224
+ ("[Autocorrelation] Moran autocorrelation", lambda: moran_autocorrelation(sequence, **_p("moran_autocorrelation"))),
225
+ ("[Autocorrelation] Geary autocorrelation", lambda: geary_autocorrelation(sequence, **_p("geary_autocorrelation"))),
226
+ # ── Conjoint Triad ─────────────────────────────────────────────────────
227
+ ("[Conjoint Triad] Conjoint triad", lambda: _conjoint_triad(sequence)),
228
+ # ── CTD ────────────────────────────────────────────────────────────────
229
+ ("[CTD] Composition, transition & distribution",lambda: ctd_(sequence, all_ctd=True, **_p("ctd_"))),
230
+ # ── Sequence Order ─────────────────────────────────────────────────────
231
+ ("[Sequence Order] Sequence order coupling number", lambda: sequence_order_coupling_number_all(sequence, **_p("sequence_order_coupling_number_all"))),
232
+ ("[Sequence Order] Quasi sequence order", lambda: quasi_sequence_order_all(sequence, **_p("quasi_sequence_order_all"))),
233
+ ]
234
+
235
+ descriptor_names = [label.split("]", 1)[1].strip() for label, _ in steps]
236
+ if _show_descriptor_list:
237
+ print("Calculating descriptors: " + ", ".join(descriptor_names))
238
+
239
+ # Load any rows already saved from previous sequences in this run so they
240
+ # are preserved when we rewrite the file after each checkpoint.
241
+ existing_df: pd.DataFrame | None = None
242
+ if output_file is not None and os.path.isfile(output_file):
243
+ try:
244
+ existing_df = pd.read_csv(output_file)
245
+ except Exception:
246
+ existing_df = None
247
+
248
+ def _checkpoint(frames_so_far: list[pd.DataFrame]) -> None:
249
+ partial = pd.concat(frames_so_far, axis=1)
250
+ out = pd.concat([existing_df, partial], ignore_index=True) if existing_df is not None else partial
251
+ out.to_csv(output_file, index=False)
252
+
253
+ frames: list[pd.DataFrame] = []
254
+
255
+ if show_progress:
256
+ bar = _tqdm(
257
+ total=len(steps),
258
+ unit="descriptor",
259
+ dynamic_ncols=True,
260
+ bar_format="{desc} {percentage:3.0f}%|{bar:28}| {n:02d}/{total} [{elapsed}<{remaining}]",
261
+ )
262
+ for label, fn in steps:
263
+ bar.set_description(label)
264
+ frames.append(fn())
265
+ bar.update(1)
266
+ bar.set_description(f"All {len(steps)} descriptors computed")
267
+ bar.close()
268
+ else:
269
+ for label, fn in steps:
270
+ frames.append(fn())
271
+
272
+ result = pd.concat(frames, axis=1)
273
+
274
+ if output_file is not None and save_each_sequence:
275
+ _checkpoint(frames)
276
+
277
+ return result
278
+
279
+
280
+ def encode_dataset(
281
+ sequences: "list[str] | pd.DataFrame",
282
+ seq_col: str = "sequence",
283
+ params: dict[str, dict] | None = None,
284
+ show_progress: bool = False,
285
+ output_file: str | None = "descriptors.csv",
286
+ save_each_sequence: bool = True,
287
+ strip_gaps: bool = False,
288
+ ) -> pd.DataFrame:
289
+ """
290
+ Compute all available descriptors for every sequence in a dataset and
291
+ return a DataFrame whose **first column is a unique sequence identifier**.
292
+
293
+ Parameters
294
+ ==========
295
+ :sequences: list[str] | pd.DataFrame
296
+ Input sequences in one of two forms:
297
+
298
+ - ``list[str]`` — a plain list of protein sequences. Identifiers are
299
+ generated automatically as ``seq1, seq2, seq3, …`` and the
300
+ identifier column is named ``"sequence_id"``.
301
+ - ``pd.DataFrame`` — typically the output of
302
+ ``read_sequences_from_file(..., id_col=…)``, which has two columns
303
+ ``[id_col, seq_col]``. The first column is used as the identifier;
304
+ ``seq_col`` selects the sequence data.
305
+
306
+ :seq_col: str (default="sequence")
307
+ Name of the column containing sequences when *sequences* is a
308
+ ``pd.DataFrame``.
309
+
310
+ :params: dict[str, dict] | None (default=None)
311
+ Per-descriptor keyword arguments forwarded to each ``all_descriptors``
312
+ call. See ``all_descriptors`` for the full list of accepted keys.
313
+
314
+ :show_progress: bool (default=False)
315
+ When ``True``, a ``tqdm`` progress bar is displayed while computing
316
+ descriptors for each individual sequence.
317
+
318
+ :output_file: str | None (default="descriptors.csv")
319
+ Path to the CSV file where results are written. Each completed row
320
+ (identifier prepended) is flushed immediately after that sequence
321
+ finishes, so progress is preserved if the process is interrupted.
322
+ Rows already present in the file from a previous run are retained.
323
+ Pass ``None`` to disable all file output.
324
+
325
+ :save_each_sequence: bool (default=True)
326
+ When ``True``, the CSV is updated after each sequence completes.
327
+ When ``False``, the file is never written inside this call. Has no
328
+ effect when ``output_file`` is ``None``.
329
+
330
+ :strip_gaps: bool (default=False)
331
+ When ``True``, gap characters (``-``) are removed from each sequence
332
+ before encoding. Useful for sequences derived from multiple sequence
333
+ alignments (MSAs) where ``-`` denotes an alignment gap rather than an
334
+ amino acid. When ``False`` (default), a ``-`` in any sequence raises
335
+ a ``ValueError``.
336
+
337
+ Returns
338
+ =======
339
+ :result: pd.DataFrame
340
+ Shape ``(n_sequences, 1 + n_features)`` where the first column holds
341
+ the sequence identifier and the remaining columns are descriptor
342
+ values. Returns an empty DataFrame when *sequences* is empty.
343
+
344
+ Raises
345
+ ======
346
+ TypeError
347
+ If *sequences* is neither a ``list`` nor a ``pd.DataFrame``.
348
+ ValueError
349
+ If *sequences* is a ``pd.DataFrame`` and *seq_col* is not present.
350
+
351
+ Examples
352
+ ========
353
+ >>> import protpy
354
+ >>> # Plain list — identifiers auto-generated as seq1, seq2, …
355
+ >>> seqs = ["MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD",
356
+ ... "ACDEFGHIKLMNPQRSTVWYACDEFGHIKLMNPQRSTVWY"]
357
+ >>> df = protpy.encode_dataset(seqs)
358
+ >>> df.columns[0]
359
+ 'sequence_id'
360
+ >>> df["sequence_id"].tolist()
361
+ ['seq1', 'seq2']
362
+
363
+ >>> # DataFrame with named identifiers (e.g. from thermostability.txt)
364
+ >>> data = protpy.read_sequences_from_file(
365
+ ... "thermostability.txt", seq_col="sequence", id_col="name"
366
+ ... )
367
+ >>> df = protpy.encode_dataset(data, seq_col="sequence")
368
+ >>> df.columns[0]
369
+ 'name'
370
+ """
371
+ if isinstance(sequences, list):
372
+ id_col_name = "sequence_id"
373
+ ids = [f"seq{i + 1}" for i in range(len(sequences))]
374
+ seqs = [str(s) for s in sequences]
375
+ elif isinstance(sequences, pd.DataFrame):
376
+ if sequences.empty:
377
+ return pd.DataFrame()
378
+ id_col_name = sequences.columns[0]
379
+ if seq_col not in sequences.columns:
380
+ raise ValueError(
381
+ f"seq_col '{seq_col}' not found in DataFrame. "
382
+ f"Available columns: {list(sequences.columns)}."
383
+ )
384
+ ids = sequences[id_col_name].tolist()
385
+ seqs = sequences[seq_col].tolist()
386
+ else:
387
+ raise TypeError(
388
+ f"sequences must be a list[str] or pd.DataFrame, got {type(sequences)}."
389
+ )
390
+
391
+ # Snapshot any previously saved rows so they are preserved on each flush.
392
+ initial_df: pd.DataFrame | None = None
393
+ if output_file is not None and os.path.isfile(output_file):
394
+ try:
395
+ initial_df = pd.read_csv(output_file)
396
+ except Exception:
397
+ pass
398
+
399
+ completed: list[pd.DataFrame] = []
400
+ total = len(seqs)
401
+
402
+ for idx, (seq_id, seq) in enumerate(zip(ids, seqs), start=1):
403
+ print(f"Encoding sequence {idx}/{total}: {seq_id}")
404
+ if strip_gaps:
405
+ seq = seq.replace("-", "")
406
+ elif "-" in seq:
407
+ raise ValueError(
408
+ f"Sequence '{seq_id}' contains gap characters ('-'). "
409
+ "If the sequences are from a multiple sequence alignment (MSA), "
410
+ "set strip_gaps=True to remove them before encoding."
411
+ )
412
+ row_df = all_descriptors(
413
+ seq,
414
+ params=params,
415
+ show_progress=show_progress,
416
+ output_file=None,
417
+ save_each_sequence=False,
418
+ _show_descriptor_list=(idx == 1),
419
+ )
420
+ row_df.insert(0, id_col_name, seq_id)
421
+ completed.append(row_df)
422
+
423
+ if output_file is not None and save_each_sequence:
424
+ new_rows = pd.concat(completed, ignore_index=True)
425
+ # Only prepend the initial snapshot when its columns exactly match the
426
+ # current run (mismatches arise from stale CSVs with mangled duplicate
427
+ # column names written by a prior buggy run).
428
+ if initial_df is not None and list(initial_df.columns) == list(new_rows.columns):
429
+ out = pd.concat([initial_df, new_rows], ignore_index=True)
430
+ else:
431
+ out = new_rows
432
+ out.to_csv(output_file, index=False)
433
+
434
+ if not completed:
435
+ return pd.DataFrame()
436
+ return pd.concat(completed, ignore_index=True)
437
+
438
+
439
+ def _parse_fasta(filepath: str) -> list[str]:
440
+ sequences: list[str] = []
441
+ current: list[str] = []
442
+ with open(filepath) as fh:
443
+ for line in fh:
444
+ line = line.strip()
445
+ if not line:
446
+ continue
447
+ if line.startswith(">"):
448
+ if current:
449
+ sequences.append("".join(current).upper())
450
+ current = []
451
+ else:
452
+ current.append(line)
453
+ if current:
454
+ sequences.append("".join(current).upper())
455
+ return sequences
456
+
457
+
458
+ def _parse_txt(filepath: str) -> list[str]:
459
+ sequences: list[str] = []
460
+ with open(filepath) as fh:
461
+ for line in fh:
462
+ seq = line.strip().upper()
463
+ if seq:
464
+ sequences.append(seq)
465
+ return sequences
466
+
467
+
468
+ def _detect_sep(filepath: str) -> str:
469
+ with open(filepath) as fh:
470
+ first_line = fh.readline()
471
+ return "\t" if "\t" in first_line else ","
472
+
473
+
474
+ def _resolve_col(
475
+ df: pd.DataFrame,
476
+ col: str | int | None,
477
+ param_name: str,
478
+ ) -> str:
479
+ if col is None:
480
+ return df.columns[0]
481
+ if isinstance(col, int):
482
+ try:
483
+ return df.columns[col]
484
+ except IndexError:
485
+ raise ValueError(
486
+ f"{param_name} index {col} is out of range for a file with "
487
+ f"{df.shape[1]} column(s)."
488
+ )
489
+ if isinstance(col, str):
490
+ if col not in df.columns:
491
+ raise ValueError(
492
+ f"{param_name} '{col}' not found. Available columns: {list(df.columns)}."
493
+ )
494
+ return col
495
+ raise TypeError(f"{param_name} must be a str, int, or None, got {type(col)}.")
496
+
497
+
498
+ def _parse_delimited(
499
+ filepath: str,
500
+ sep: str,
501
+ seq_col: str | int | None,
502
+ id_col: str | int | None,
503
+ has_header: bool,
504
+ ) -> "list[str] | pd.DataFrame":
505
+ df = pd.read_csv(filepath, sep=sep, header=0 if has_header else None)
506
+
507
+ seq_label = _resolve_col(df, seq_col, "seq_col")
508
+
509
+ if id_col is None:
510
+ raw = df[seq_label]
511
+ return [str(s).strip().upper() for s in raw if pd.notna(s) and str(s).strip()]
512
+
513
+ id_label = _resolve_col(df, id_col, "id_col")
514
+ result = df[[id_label, seq_label]].copy()
515
+ result = result[result[seq_label].notna()].copy()
516
+ result[seq_label] = result[seq_label].astype(str).str.strip().str.upper()
517
+ result = result[result[seq_label].str.len() > 0].reset_index(drop=True)
518
+ return result
519
+
520
+
521
+ def read_sequences_from_file(
522
+ filepath: str | os.PathLike,
523
+ seq_col: str | int | None = None,
524
+ id_col: str | int | None = None,
525
+ has_header: bool = True,
526
+ ) -> "list[str] | pd.DataFrame":
527
+ """
528
+ Parse protein sequences from a FASTA, TXT, TSV, or CSV file.
529
+
530
+ The file format is inferred from the extension:
531
+
532
+ - ``.fasta`` / ``.fa`` — standard FASTA format; multi-line sequences are
533
+ joined automatically; every record in the file is returned as a
534
+ ``list[str]``.
535
+ - ``.txt`` without ``seq_col`` — one raw sequence per non-blank line,
536
+ returned as a ``list[str]``.
537
+ - ``.txt`` with ``seq_col`` — treated as a delimited file; the separator
538
+ (``,`` or ``\\t``) is auto-detected from the first line.
539
+ - ``.csv`` — comma-separated values.
540
+ - ``.tsv`` — tab-separated values.
541
+
542
+ When ``id_col`` is provided the function returns a ``pd.DataFrame``
543
+ containing only the identifier and sequence columns; all other columns
544
+ are dropped and rows with blank sequences are removed. When ``id_col``
545
+ is omitted the function returns a plain ``list[str]`` of sequences.
546
+
547
+ Parameters
548
+ ==========
549
+ :filepath: str | os.PathLike
550
+ Path to the input file.
551
+
552
+ :seq_col: str | int | None (default=None)
553
+ The column that contains sequences. Pass a column *name* (str) to
554
+ select by header, or a zero-based *index* (int) to select by
555
+ position. ``None`` selects the first column. Ignored for plain
556
+ FASTA and plain TXT files.
557
+
558
+ :id_col: str | int | None (default=None)
559
+ The column that identifies each sequence row (e.g. ``"name"``).
560
+ When provided, all other columns are dropped and a ``pd.DataFrame``
561
+ with columns ``[id_col, seq_col]`` is returned. ``None`` returns a
562
+ ``list[str]`` of sequences only. Ignored for FASTA files.
563
+
564
+ :has_header: bool (default=True)
565
+ Whether the first row of a delimited file is a header. Set to
566
+ ``False`` for headerless files (column names are then integers).
567
+ Ignored for FASTA and plain TXT files.
568
+
569
+ Returns
570
+ =======
571
+ :result: list[str] | pd.DataFrame
572
+ - ``list[str]`` when ``id_col`` is ``None``: uppercase sequences,
573
+ blank entries excluded.
574
+ - ``pd.DataFrame`` when ``id_col`` is given: two-column frame with
575
+ the identifier and (uppercased) sequence columns; rows with blank
576
+ sequences are excluded and the index is reset.
577
+
578
+ Raises
579
+ ======
580
+ FileNotFoundError
581
+ If the file at ``filepath`` does not exist.
582
+ ValueError
583
+ If the extension is unsupported, or if a requested column is not
584
+ found in a delimited file.
585
+ TypeError
586
+ If ``seq_col`` or ``id_col`` is not a str, int, or None.
587
+
588
+ Examples
589
+ ========
590
+ >>> import protpy
591
+ >>> # FASTA / plain TXT
592
+ >>> seqs = protpy.read_sequences_from_file("proteins.fasta")
593
+ >>> seqs = protpy.read_sequences_from_file("seqs.txt")
594
+ >>> # CSV / TSV — sequences only
595
+ >>> seqs = protpy.read_sequences_from_file("data.csv", seq_col="sequence")
596
+ >>> seqs = protpy.read_sequences_from_file("data.tsv", seq_col=1)
597
+ >>> # Multi-column TXT/CSV/TSV — keep id + sequence, drop everything else
598
+ >>> df = protpy.read_sequences_from_file(
599
+ ... "thermostability.txt", seq_col="sequence", id_col="name"
600
+ ... )
601
+ """
602
+ filepath = str(filepath)
603
+
604
+ if not os.path.isfile(filepath):
605
+ raise FileNotFoundError(f"No such file: '{filepath}'")
606
+
607
+ ext = os.path.splitext(filepath)[1].lower()
608
+
609
+ if ext in (".fasta", ".fa"):
610
+ return _parse_fasta(filepath)
611
+ elif ext == ".txt":
612
+ if seq_col is not None:
613
+ sep = _detect_sep(filepath)
614
+ return _parse_delimited(filepath, sep=sep, seq_col=seq_col, id_col=id_col, has_header=has_header)
615
+ return _parse_txt(filepath)
616
+ elif ext == ".csv":
617
+ return _parse_delimited(filepath, sep=",", seq_col=seq_col, id_col=id_col, has_header=has_header)
618
+ elif ext == ".tsv":
619
+ return _parse_delimited(filepath, sep="\t", seq_col=seq_col, id_col=id_col, has_header=has_header)
620
+ else:
621
+ raise ValueError(
622
+ f"Unsupported file extension '{ext}'. "
623
+ "Expected one of: .fasta, .fa, .txt, .csv, .tsv."
624
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: protpy
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences.
5
5
  Author-email: AJ McKenna <amckenna41@qub.ac.uk>
6
6
  Maintainer-email: AJ McKenna <amckenna41@qub.ac.uk>
@@ -35,6 +35,7 @@ License-File: LICENSE
35
35
  Requires-Dist: aaindex>=1.2.0
36
36
  Requires-Dist: numpy>=2.4.4
37
37
  Requires-Dist: pandas>=3.0.2
38
+ Requires-Dist: tqdm>=4.0.0
38
39
  Requires-Dist: varname>=0.15.1
39
40
  Provides-Extra: test
40
41
  Requires-Dist: pytest; extra == "test"