protein-quest 0.10.0__tar.gz → 0.10.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 (94) hide show
  1. {protein_quest-0.10.0 → protein_quest-0.10.1}/.github/workflows/ci.yml +10 -1
  2. protein_quest-0.10.1/.howfairis.yml +1 -0
  3. {protein_quest-0.10.0 → protein_quest-0.10.1}/CONTRIBUTING.md +2 -1
  4. {protein_quest-0.10.0 → protein_quest-0.10.1}/PKG-INFO +8 -3
  5. {protein_quest-0.10.0 → protein_quest-0.10.1}/README.md +7 -2
  6. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/__version__.py +1 -1
  7. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/alphafold/fetch.py +2 -1
  8. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/parallel.py +63 -3
  9. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/structure.py +12 -0
  10. protein_quest-0.10.1/tests/cassettes/test_cli/test_search_alphafold.yaml +66 -0
  11. protein_quest-0.10.1/tests/test_cli.py +305 -0
  12. protein_quest-0.10.1/tests/test_filters.py +78 -0
  13. protein_quest-0.10.0/tests/test_cli.py +0 -101
  14. {protein_quest-0.10.0 → protein_quest-0.10.1}/.github/workflows/pages.yml +0 -0
  15. {protein_quest-0.10.0 → protein_quest-0.10.1}/.github/workflows/pypi-publish.yml +0 -0
  16. {protein_quest-0.10.0 → protein_quest-0.10.1}/.gitignore +0 -0
  17. {protein_quest-0.10.0 → protein_quest-0.10.1}/.python-version +0 -0
  18. {protein_quest-0.10.0 → protein_quest-0.10.1}/.vscode/extensions.json +0 -0
  19. {protein_quest-0.10.0 → protein_quest-0.10.1}/CITATION.cff +0 -0
  20. {protein_quest-0.10.0 → protein_quest-0.10.1}/CODE_OF_CONDUCT.md +0 -0
  21. {protein_quest-0.10.0 → protein_quest-0.10.1}/LICENSE +0 -0
  22. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/CONTRIBUTING.md +0 -0
  23. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/index.md +0 -0
  24. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/notebooks/.gitignore +0 -0
  25. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/notebooks/alphafold.ipynb +0 -0
  26. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/notebooks/index.md +0 -0
  27. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/notebooks/pdbe.ipynb +0 -0
  28. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/notebooks/uniprot.ipynb +0 -0
  29. {protein_quest-0.10.0 → protein_quest-0.10.1}/docs/protein-quest-mcp.png +0 -0
  30. {protein_quest-0.10.0 → protein_quest-0.10.1}/mkdocs.yml +0 -0
  31. {protein_quest-0.10.0 → protein_quest-0.10.1}/pyproject.toml +0 -0
  32. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/__init__.py +0 -0
  33. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/alphafold/__init__.py +0 -0
  34. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/alphafold/confidence.py +0 -0
  35. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/alphafold/entry_summary.py +0 -0
  36. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/cli.py +0 -0
  37. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/converter.py +0 -0
  38. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/emdb.py +0 -0
  39. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/filters.py +0 -0
  40. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/go.py +0 -0
  41. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/io.py +0 -0
  42. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/mcp_server.py +0 -0
  43. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/pdbe/__init__.py +0 -0
  44. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/pdbe/fetch.py +0 -0
  45. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/py.typed +0 -0
  46. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/ss.py +0 -0
  47. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/taxonomy.py +0 -0
  48. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/uniprot.py +0 -0
  49. {protein_quest-0.10.0 → protein_quest-0.10.1}/src/protein_quest/utils.py +0 -0
  50. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/AF-A1YPR0-F1-model_v4.pdb +0 -0
  51. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/cassettes/test_fetch/test_fetch_alphafold_db_version.yaml +0 -0
  52. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/cassettes/test_fetch/test_fetch_many.yaml +0 -0
  53. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/cassettes/test_fetch/test_fetch_many_all_isoforms.yaml +0 -0
  54. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/cassettes/test_fetch/test_fetch_many_gzipped.yaml +0 -0
  55. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/cassettes/test_fetch/test_fetch_many_no_summary.yaml +0 -0
  56. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/cassettes/test_fetch/test_fetch_many_no_summary_with_version.yaml +0 -0
  57. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/test_confidence.py +0 -0
  58. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/test_entry_summary.py +0 -0
  59. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/alphafold/test_fetch.py +0 -0
  60. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_cli/test_search_pdbe.yaml +0 -0
  61. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_cli/test_search_uniprot.yaml +0 -0
  62. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_cli/test_search_uniprot_details.yaml +0 -0
  63. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_emdb/test_fetch.yaml +0 -0
  64. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_go/test_search_gene_ontology_term.yaml +0 -0
  65. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_taxonomy/test_search_taxon.yaml +0 -0
  66. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_taxonomy/test_search_taxon_by_id.yaml +0 -0
  67. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/TestSearch4AfExternalIsoforms.test_do_not_match_external_isoform.yaml +0 -0
  68. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/TestSearch4AfExternalIsoforms.test_match_canonical_isoform.yaml +0 -0
  69. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_map_uniprot_accessions2uniprot_details.yaml +0 -0
  70. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4af.yaml +0 -0
  71. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4af_ok_sequence_length.yaml +0 -0
  72. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4af_too_big_sequence_length.yaml +0 -0
  73. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4af_too_small_sequence_length.yaml +0 -0
  74. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4emdb.yaml +0 -0
  75. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4interaction_partners.yaml +0 -0
  76. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4macromolecular_complexes.yaml +0 -0
  77. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4pdb.yaml +0 -0
  78. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/cassettes/test_uniprot/test_search4uniprot.yaml +0 -0
  79. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/conftest.py +0 -0
  80. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/fixtures/2Y29.cif.gz +0 -0
  81. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/fixtures/3JRS_B2A.cif.gz +0 -0
  82. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/pdbe/cassettes/test_fetch/test_fetch.yaml +0 -0
  83. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/pdbe/test_fetch.py +0 -0
  84. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_converter.py +0 -0
  85. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_emdb.py +0 -0
  86. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_go.py +0 -0
  87. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_io.py +0 -0
  88. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_mcp.py +0 -0
  89. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_ss.py +0 -0
  90. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_structure.py +0 -0
  91. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_taxonomy.py +0 -0
  92. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_uniprot.py +0 -0
  93. {protein_quest-0.10.0 → protein_quest-0.10.1}/tests/test_utils.py +0 -0
  94. {protein_quest-0.10.0 → protein_quest-0.10.1}/uv.lock +0 -0
@@ -81,4 +81,13 @@ jobs:
81
81
  find docs/ -name "*.ipynb" -exec uv run --group docs-type marimo convert {} -o {}.py \;
82
82
  - name: Run type checkers on docs
83
83
  run: uv run --group docs-type pyrefly check docs/notebooks/*.ipynb.py
84
-
84
+ duplicated-code:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - name: Install NodeJS
89
+ uses: actions/setup-node@v6
90
+ with:
91
+ node-version: '24'
92
+ - name: Run jscpd to detect duplicated code
93
+ run: npx jscpd src
@@ -0,0 +1 @@
1
+ skip_checklist_checks_reason: "I'm using the fairsoftwarechecklist"
@@ -38,7 +38,8 @@ The sections below outline the steps in each case.
38
38
  1. format your code with `uvx ruff format` and sort imports with `uvx ruff check --select I --fix`;
39
39
  1. lint your code with `uvx ruff check` (use `uvx ruff check --fix` to fix issues automatically);
40
40
  1. type check your code with `uv run pyrefly check src tests`;
41
- 1. update or expand the documentation (see [Contributing with documentation](#contributing-with-documentation) section below);
41
+ 1. prevent code duplication, detect with `npx jscpd src`;
42
+ 1. update or expand the documentation (see [Contributing to documentation](#contributing-to-documentation) section below);
42
43
  1. [push](http://rogerdudler.github.io/git-guide/) your feature branch to (your fork of) the protein-quest repository on GitHub;
43
44
  1. create the pull request, e.g. following the instructions [here](https://help.github.com/articles/creating-a-pull-request/).
44
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: protein_quest
3
- Version: 0.10.0
3
+ Version: 0.10.1
4
4
  Summary: Search/retrieve/filter proteins and protein structures
5
5
  Project-URL: Homepage, https://github.com/haddocking/protein-quest
6
6
  Project-URL: Issues, https://github.com/haddocking/protein-quest/issues
@@ -35,9 +35,14 @@ Description-Content-Type: text/markdown
35
35
  [![Documentation](https://img.shields.io/badge/Documentation-bonvinlab.org-blue?style=flat-square&logo=gitbook)](https://www.bonvinlab.org/protein-quest/)
36
36
  [![CI](https://github.com/haddocking/protein-quest/actions/workflows/ci.yml/badge.svg)](https://github.com/haddocking/protein-quest/actions/workflows/ci.yml)
37
37
  [![Research Software Directory Badge](https://img.shields.io/badge/rsd-00a3e3.svg)](https://www.research-software.nl/software/protein-quest)
38
+ [![bio.tools](https://img.shields.io/badge/bio.tools-protein--quest-009fdf.svg)](https://bio.tools/protein-quest)
38
39
  [![PyPI](https://img.shields.io/pypi/v/protein-quest)](https://pypi.org/project/protein-quest/)
39
40
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.16941288.svg)](https://doi.org/10.5281/zenodo.16941288)
40
41
  [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/7a3f3f1fe64640d583a5e50fe7ba828e)](https://app.codacy.com/gh/haddocking/protein-quest/coverage?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
42
+ [![FAIR checklist badge](https://fairsoftwarechecklist.net/badge.svg)](https://fairsoftwarechecklist.net/v0.2?f=31&a=32113&i=32121&r=133)
43
+ [![fair-software.eu](https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F-green)](https://fair-software.eu)
44
+ [![Copy/paste detector](https://raw.githubusercontent.com/kucherenko/jscpd/refs/tags/v3.5.10/assets/jscpd-badge.svg?sanitize=true)](https://github.com/kucherenko/jscpd/)
45
+
41
46
 
42
47
  Python package to search/retrieve/filter proteins and protein structures.
43
48
 
@@ -104,7 +109,7 @@ pip install git+https://github.com/haddocking/protein-quest.git
104
109
 
105
110
  The main entry point is the `protein-quest` command line tool which has multiple subcommands to perform actions.
106
111
 
107
- To use programmaticly, see the [Jupyter notebooks](https://www.bonvinlab.org/protein-quest/notebooks) and [API documentation](https://www.bonvinlab.org/protein-quest/autoapi/summary/).
112
+ To use programmaticly, see the [Jupyter notebooks](https://www.bonvinlab.org/protein-quest/notebooks) and [API documentation](https://www.bonvinlab.org/protein-quest/autoapi/protein_quest/).
108
113
 
109
114
  While downloading or copying files it uses a global cache (located at `~/.cache/protein-quest`) and hardlinks to save disk space and improve speed.
110
115
  This behavior can be customized with the `--no-cache`, `--cache-dir`, and `--copy-method` command line arguments.
@@ -302,7 +307,7 @@ The mcp server contains an prompt template to search/retrieve/filter candidate s
302
307
 
303
308
  ## Shell autocompletion
304
309
 
305
- The `protein-quest` command line tool supports shell autocompletion using [shtab](https://shtab.readthedocs.io/).
310
+ The `protein-quest` command line tool supports shell autocompletion using [shtab](https://docs.iterative.ai/shtab).
306
311
 
307
312
  Initialize for bash shell with:
308
313
 
@@ -3,9 +3,14 @@
3
3
  [![Documentation](https://img.shields.io/badge/Documentation-bonvinlab.org-blue?style=flat-square&logo=gitbook)](https://www.bonvinlab.org/protein-quest/)
4
4
  [![CI](https://github.com/haddocking/protein-quest/actions/workflows/ci.yml/badge.svg)](https://github.com/haddocking/protein-quest/actions/workflows/ci.yml)
5
5
  [![Research Software Directory Badge](https://img.shields.io/badge/rsd-00a3e3.svg)](https://www.research-software.nl/software/protein-quest)
6
+ [![bio.tools](https://img.shields.io/badge/bio.tools-protein--quest-009fdf.svg)](https://bio.tools/protein-quest)
6
7
  [![PyPI](https://img.shields.io/pypi/v/protein-quest)](https://pypi.org/project/protein-quest/)
7
8
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.16941288.svg)](https://doi.org/10.5281/zenodo.16941288)
8
9
  [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/7a3f3f1fe64640d583a5e50fe7ba828e)](https://app.codacy.com/gh/haddocking/protein-quest/coverage?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
10
+ [![FAIR checklist badge](https://fairsoftwarechecklist.net/badge.svg)](https://fairsoftwarechecklist.net/v0.2?f=31&a=32113&i=32121&r=133)
11
+ [![fair-software.eu](https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F-green)](https://fair-software.eu)
12
+ [![Copy/paste detector](https://raw.githubusercontent.com/kucherenko/jscpd/refs/tags/v3.5.10/assets/jscpd-badge.svg?sanitize=true)](https://github.com/kucherenko/jscpd/)
13
+
9
14
 
10
15
  Python package to search/retrieve/filter proteins and protein structures.
11
16
 
@@ -72,7 +77,7 @@ pip install git+https://github.com/haddocking/protein-quest.git
72
77
 
73
78
  The main entry point is the `protein-quest` command line tool which has multiple subcommands to perform actions.
74
79
 
75
- To use programmaticly, see the [Jupyter notebooks](https://www.bonvinlab.org/protein-quest/notebooks) and [API documentation](https://www.bonvinlab.org/protein-quest/autoapi/summary/).
80
+ To use programmaticly, see the [Jupyter notebooks](https://www.bonvinlab.org/protein-quest/notebooks) and [API documentation](https://www.bonvinlab.org/protein-quest/autoapi/protein_quest/).
76
81
 
77
82
  While downloading or copying files it uses a global cache (located at `~/.cache/protein-quest`) and hardlinks to save disk space and improve speed.
78
83
  This behavior can be customized with the `--no-cache`, `--cache-dir`, and `--copy-method` command line arguments.
@@ -270,7 +275,7 @@ The mcp server contains an prompt template to search/retrieve/filter candidate s
270
275
 
271
276
  ## Shell autocompletion
272
277
 
273
- The `protein-quest` command line tool supports shell autocompletion using [shtab](https://shtab.readthedocs.io/).
278
+ The `protein-quest` command line tool supports shell autocompletion using [shtab](https://docs.iterative.ai/shtab).
274
279
 
275
280
  Initialize for bash shell with:
276
281
 
@@ -1,2 +1,2 @@
1
- __version__ = "0.10.0"
1
+ __version__ = "0.10.1"
2
2
  """The version of the package."""
@@ -114,7 +114,6 @@ class AlphaFoldEntry:
114
114
  """Convert paths in an AlphaFoldEntry to be relative to the session directory.
115
115
 
116
116
  Args:
117
- entry: An AlphaFoldEntry instance with absolute paths.
118
117
  session_dir: The session directory to which the paths should be made relative.
119
118
 
120
119
  Returns:
@@ -483,6 +482,7 @@ def fetch_many_async(
483
482
  )
484
483
 
485
484
 
485
+ # jscpd:ignore-start # noqa: ERA001
486
486
  def fetch_many(
487
487
  uniprot_accessions: Iterable[str],
488
488
  save_dir: Path,
@@ -492,6 +492,7 @@ def fetch_many(
492
492
  cacher: Cacher | None = None,
493
493
  gzip_files: bool = False,
494
494
  all_isoforms: bool = False,
495
+ # jscpd:ignore-end # noqa: ERA001
495
496
  ) -> list[AlphaFoldEntry]:
496
497
  """Synchronously fetches summaries and/or files like cif from AlphaFold Protein Structure Database.
497
498
 
@@ -2,13 +2,19 @@
2
2
 
3
3
  import logging
4
4
  import os
5
+ import sys
6
+ import warnings
5
7
  from collections.abc import Callable, Collection, Iterator
6
- from contextlib import contextmanager
8
+ from contextlib import contextmanager, suppress
7
9
  from typing import Concatenate, ParamSpec, cast
8
10
 
9
- from dask.distributed import Client, LocalCluster, progress
11
+ from dask.distributed import Client, LocalCluster
10
12
  from distributed.deploy.cluster import Cluster
13
+ from distributed.diagnostics.progress import format_time
14
+ from distributed.diagnostics.progressbar import ProgressBar
15
+ from distributed.utils import LoopRunner
11
16
  from psutil import cpu_count
17
+ from tornado.ioloop import IOLoop
12
18
 
13
19
  logger = logging.getLogger(__name__)
14
20
 
@@ -84,6 +90,60 @@ def _configure_cpu_dask_scheduler(nproc: int, name: str) -> LocalCluster:
84
90
  P = ParamSpec("P")
85
91
 
86
92
 
93
+ class _StderrTextProgressBar(ProgressBar):
94
+ """Copy of distributed.diagnostics.progressbar.TextProgressBar that prints to stderr instead of stdout."""
95
+
96
+ __loop: IOLoop | None = None
97
+
98
+ def __init__(
99
+ self,
100
+ keys,
101
+ scheduler=None,
102
+ interval="100ms",
103
+ width=40,
104
+ loop=None,
105
+ complete=True,
106
+ start=True,
107
+ **kwargs, # noqa: ARG002
108
+ ):
109
+ self._loop_runner = loop_runner = LoopRunner(loop=loop)
110
+ super().__init__(keys, scheduler, interval, complete)
111
+ self.width = width
112
+
113
+ if start:
114
+ loop_runner.run_sync(self.listen)
115
+
116
+ @property
117
+ def loop(self) -> IOLoop | None:
118
+ loop = self.__loop
119
+ if loop is None:
120
+ # If the loop is not running when this is called, the LoopRunner.loop
121
+ # property will raise a DeprecationWarning
122
+ # However subsequent calls might occur - eg atexit, where a stopped
123
+ # loop is still acceptable - so we cache access to the loop.
124
+ self.__loop = loop = self._loop_runner.loop
125
+ return loop
126
+
127
+ @loop.setter
128
+ def loop(self, value: IOLoop) -> None:
129
+ warnings.warn("setting the loop property is deprecated", DeprecationWarning, stacklevel=2)
130
+ self.__loop = value
131
+
132
+ def _draw_bar(self, remaining, all, **kwargs): # noqa: A002, ARG002
133
+ frac = (1 - remaining / all) if all else 1.0
134
+ bar = "#" * int(self.width * frac)
135
+ percent = int(100 * frac)
136
+ elapsed = format_time(self.elapsed)
137
+ msg = "\r[{0:<{1}}] | {2}% Completed | {3}".format(bar, self.width, percent, elapsed)
138
+ with suppress(ValueError):
139
+ sys.stderr.write(msg)
140
+ sys.stderr.flush()
141
+
142
+ def _draw_stop(self, **kwargs): # noqa: ARG002
143
+ sys.stderr.write("\33[2K\r")
144
+ sys.stderr.flush()
145
+
146
+
87
147
  def dask_map_with_progress[T, R, **P](
88
148
  client: Client,
89
149
  func: Callable[Concatenate[T, P], R],
@@ -109,6 +169,6 @@ def dask_map_with_progress[T, R, **P](
109
169
  if client.dashboard_link:
110
170
  logger.info(f"Follow progress on dask dashboard at: {client.dashboard_link}")
111
171
  futures = client.map(func, iterable, *args, **kwargs)
112
- progress(futures)
172
+ _StderrTextProgressBar(futures)
113
173
  results = client.gather(futures)
114
174
  return cast("list[R]", results)
@@ -132,6 +132,18 @@ class ChainNotFoundError(IndexError):
132
132
  """Helper for pickling the exception."""
133
133
  return (self.__class__, (self.chain_id, self.file, self.available_chains))
134
134
 
135
+ def __eq__(self, other):
136
+ if not isinstance(other, ChainNotFoundError):
137
+ return NotImplemented
138
+ return (
139
+ self.chain_id == other.chain_id
140
+ and self.file == other.file
141
+ and self.available_chains == other.available_chains
142
+ )
143
+
144
+ def __hash__(self):
145
+ return hash((self.chain_id, str(self.file), frozenset(self.available_chains)))
146
+
135
147
 
136
148
  def write_single_chain_structure_file(
137
149
  input_file: Path,
@@ -0,0 +1,66 @@
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - application/sparql-results+json,application/json,text/javascript,application/javascript
7
+ Connection:
8
+ - close
9
+ Host:
10
+ - sparql.uniprot.org
11
+ User-Agent:
12
+ - sparqlwrapper 2.0.0 (rdflib.github.io/sparqlwrapper)
13
+ method: GET
14
+ uri: https://sparql.uniprot.org/sparql?query=%0A++++++++PREFIX+up%3A+%3Chttp%3A//purl.uniprot.org/core/%3E%0A++++++++PREFIX+taxon%3A+%3Chttp%3A//purl.uniprot.org/taxonomy/%3E%0A++++++++PREFIX+rdf%3A+%3Chttp%3A//www.w3.org/1999/02/22-rdf-syntax-ns%23%3E%0A++++++++PREFIX+rdfs%3A+%3Chttp%3A//www.w3.org/2000/01/rdf-schema%23%3E%0A++++++++PREFIX+skos%3A+%3Chttp%3A//www.w3.org/2004/02/skos/core%23%3E%0A++++++++PREFIX+GO%3A%3Chttp%3A//purl.obolibrary.org/obo/GO_%3E%0A%0A++++++++SELECT+%3Fprotein+%3Faf_db%0A++++++++WHERE+%7B%0A%0A++++++++%23+---+Protein+Selection+---%0A++++++++VALUES+%28%3Fac%29+%7B+%28%22P00811%22%29%7D%0A++++++++BIND+%28IRI%28CONCAT%28%22http%3A//purl.uniprot.org/uniprot/%22%2C%3Fac%29%29+AS+%3Fprotein%29%0A++++++++%3Fprotein+a+up%3AProtein+.%0A%0A%0A%23+---+Protein+Selection+---%0A%3Fprotein+a+up%3AProtein+.%0A%0A%23+---+AlphaFoldDB+Info+---%0A%3Fprotein+rdfs%3AseeAlso+%3Faf_db+.%0A%3Faf_db+up%3Adatabase+%3Chttp%3A//purl.uniprot.org/database/AlphaFoldDB%3E+.%0A%0A%0A++++++++%7D%0A%0A++++++++LIMIT+10000%0A&format=json&output=json&results=json
15
+ response:
16
+ body:
17
+ string: "{\n \"head\" : {\n \"vars\" : [\n \"protein\",\n \"af_db\"\n
18
+ \ ]\n },\n \"results\" : {\n \"bindings\" : [\n {\n \"protein\"
19
+ : {\n \"type\" : \"uri\",\n \"value\" : \"http://purl.uniprot.org/uniprot/P00811\"\n
20
+ \ },\n \"af_db\" : {\n \"type\" : \"uri\",\n \"value\"
21
+ : \"http://purl.uniprot.org/alphafolddb/P00811\"\n }\n }\n ]\n
22
+ \ }\n}"
23
+ headers:
24
+ Access-Control-Allow-Headers:
25
+ - origin, x-requested-with, content-type, X-Release, queryid
26
+ Access-Control-Allow-Origin:
27
+ - '*'
28
+ Access-Control-Expose-Headers:
29
+ - X-Total-Results, X-Release, queryid, content-type, user-agent, cache-control,
30
+ etag, range
31
+ Cache-Control:
32
+ - public
33
+ Connection:
34
+ - close
35
+ Content-Disposition:
36
+ - attachment; filename="sparql-CA32A0B92DC5589CE5CD9BF33CF492F9.srj"
37
+ Content-Length:
38
+ - '375'
39
+ Content-Type:
40
+ - application/sparql-results+json
41
+ Date:
42
+ - Mon, 17 Nov 2025 11:45:48 GMT
43
+ ETag:
44
+ - W/"2025_04"
45
+ Expires:
46
+ - Tue, 18 Nov 2025 11:45:48 GMT
47
+ Server:
48
+ - Apache
49
+ Strict-Transport-Security:
50
+ - max-age=31536001; includeSubDomains
51
+ Vary:
52
+ - Negotiate,Accept,Accept-Encoding,Content-Type
53
+ X-Content-Type-Options:
54
+ - nosniff
55
+ X-Frame-Options:
56
+ - SAMEORIGIN
57
+ X-Powered-By:
58
+ - sib.swiss
59
+ X-Release:
60
+ - '2025_04'
61
+ queryid:
62
+ - '770887'
63
+ status:
64
+ code: 200
65
+ message: ''
66
+ version: 1
@@ -0,0 +1,305 @@
1
+ import csv
2
+ from pathlib import Path
3
+ from textwrap import dedent
4
+
5
+ import pytest
6
+
7
+ from protein_quest.cli import main, make_parser
8
+
9
+
10
+ def test_make_parser_help(capsys: pytest.CaptureFixture[str]):
11
+ in_args = ["--help"]
12
+ parser = make_parser()
13
+ with pytest.raises(SystemExit):
14
+ parser.parse_args(in_args)
15
+
16
+ captured = capsys.readouterr()
17
+ assert "Protein Quest CLI" in captured.out
18
+
19
+
20
+ @pytest.mark.vcr
21
+ def test_search_uniprot(capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture):
22
+ argv = [
23
+ "search",
24
+ "uniprot",
25
+ "--taxon-id",
26
+ "9606",
27
+ "--reviewed",
28
+ "--limit",
29
+ "1",
30
+ "-",
31
+ ]
32
+
33
+ main(argv)
34
+
35
+ captured = capsys.readouterr()
36
+ expected = "A0A024R1R8\n"
37
+ assert captured.out == expected
38
+ assert "Searching for UniProt accessions" in captured.err
39
+ assert "Found 1 UniProt accessions, written to <stdout>" in captured.err
40
+ assert "There may be more results available" in caplog.text
41
+
42
+
43
+ @pytest.mark.vcr
44
+ def test_search_pdbe(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
45
+ input_text = tmp_path / "uniprot_accessions.txt"
46
+ input_text.write_text("P00811\n")
47
+ output_file = tmp_path / "pdbe_results.csv"
48
+ argv = [
49
+ "search",
50
+ "pdbe",
51
+ "--limit",
52
+ "150",
53
+ "--min-residues",
54
+ "360", # P00811 has 377 residues and 5 full PDB entries
55
+ str(input_text),
56
+ str(output_file),
57
+ ]
58
+
59
+ main(argv)
60
+
61
+ result = output_file.read_text()
62
+ expected = dedent("""\
63
+ uniprot_accession,pdb_id,method,resolution,uniprot_chains,chain,chain_length
64
+ P00811,9C6P,X-Ray_Crystallography,1.66,A/B=1-377,A,377
65
+ P00811,9C81,X-Ray_Crystallography,1.7,A/B=1-377,A,377
66
+ P00811,9C83,X-Ray_Crystallography,2.9,A/B=1-377,A,377
67
+ P00811,9C84,X-Ray_Crystallography,1.7,A/B=1-377,A,377
68
+ P00811,9DHL,X-Ray_Crystallography,1.88,A/B=1-377,A,377
69
+ """)
70
+ assert result == expected
71
+
72
+ captured = capsys.readouterr()
73
+ assert "Finding PDB entries for 1 uniprot accessions" in captured.err
74
+ assert "Before filtering found 120 PDB entries for 1 uniprot accessions." in captured.err
75
+ assert "After filtering on chain length (360, None) remained 5 PDB entries for 1 uniprot" in captured.err
76
+ assert "Written to " in captured.err
77
+
78
+
79
+ @pytest.mark.vcr
80
+ def test_search_uniprot_details(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
81
+ input_text = tmp_path / "uniprot_accessions.txt"
82
+ input_text.write_text("P05067\nA0A0B5AC95\n")
83
+ output_csv = tmp_path / "uniprot_details.csv"
84
+ argv = [
85
+ "search",
86
+ "uniprot-details",
87
+ str(input_text),
88
+ str(output_csv),
89
+ ]
90
+
91
+ main(argv)
92
+
93
+ result = output_csv.read_text()
94
+ expected = dedent("""\
95
+ uniprot_accession,uniprot_id,sequence_length,reviewed,protein_name,taxon_id,taxon_name
96
+ A0A0B5AC95,INS1A_CONGE,115,True,Con-Ins G1a,6491,Conus geographus
97
+ P05067,A4_HUMAN,770,True,Amyloid-beta precursor protein,9606,Homo sapiens
98
+ """)
99
+ assert result == expected
100
+ captured = capsys.readouterr()
101
+ assert "Retrieving UniProt entry details for 2 uniprot accessions" in captured.err
102
+ assert "Retrieved details for 2 UniProt entries, written to " in captured.err
103
+
104
+
105
+ @pytest.mark.vcr
106
+ def test_search_alphafold(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
107
+ input_text = tmp_path / "uniprot_accessions.txt"
108
+ input_text.write_text("P00811\n")
109
+ output_file = tmp_path / "af_results.csv"
110
+
111
+ argv = [
112
+ "search",
113
+ "alphafold",
114
+ str(input_text),
115
+ str(output_file),
116
+ ]
117
+
118
+ main(argv)
119
+
120
+ result = output_file.read_text()
121
+
122
+ expected = dedent("""\
123
+ uniprot_accession,af_id
124
+ P00811,P00811
125
+ """)
126
+ assert result == expected
127
+
128
+ captured = capsys.readouterr()
129
+ assert "Finding AlphaFold entries for 1 uniprot accessions" in captured.err
130
+ assert "Found 1 AlphaFold entries, written to " in captured.err
131
+
132
+
133
+ def test_filter_chain_happy_path(sample2_cif: Path, tmp_path: Path, capsys: pytest.CaptureFixture[str]):
134
+ chains_fn = tmp_path / "chains.csv"
135
+ chains_fn.write_text("pdb_id,chain\n2Y29,A\n")
136
+
137
+ argv = [
138
+ "filter",
139
+ "chain",
140
+ str(chains_fn),
141
+ str(sample2_cif.parent),
142
+ str(tmp_path),
143
+ ]
144
+
145
+ main(argv)
146
+
147
+ output_file = tmp_path / "2Y29_A2A.cif.gz"
148
+ assert output_file.exists()
149
+
150
+ captured = capsys.readouterr()
151
+ assert "Wrote 1 single-chain PDB/mmCIF files to" in captured.err
152
+
153
+
154
+ def test_filter_chain_input_file_notfound(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
155
+ input_dir = tmp_path / "input"
156
+ input_dir.mkdir()
157
+ output_dir = tmp_path / "output"
158
+ output_dir.mkdir()
159
+ chains_fn = tmp_path / "chains.csv"
160
+ chains_fn.write_text("pdb_id,chain\n2Y29,A\n")
161
+
162
+ argv = [
163
+ "filter",
164
+ "chain",
165
+ str(chains_fn),
166
+ str(input_dir),
167
+ str(output_dir),
168
+ ]
169
+
170
+ with pytest.raises(SystemExit):
171
+ main(argv)
172
+
173
+ assert not any(output_dir.iterdir())
174
+
175
+ captured = capsys.readouterr()
176
+ assert "No structure file found for 2Y29" in captured.err
177
+
178
+
179
+ def test_filter_residue(sample_cif: Path, sample2_cif: Path, tmp_path: Path, capsys: pytest.CaptureFixture[str]):
180
+ input_dir = tmp_path / "input"
181
+ input_dir.mkdir()
182
+ local_sample = input_dir / sample_cif.name
183
+ local_sample.symlink_to(sample_cif)
184
+ local_sample2 = input_dir / sample2_cif.name
185
+ local_sample2.symlink_to(sample2_cif)
186
+ output_dir = tmp_path / "output"
187
+ output_dir.mkdir()
188
+ stats_fn = tmp_path / "stats.csv"
189
+
190
+ argv = [
191
+ "filter",
192
+ "residue",
193
+ str(input_dir),
194
+ str(output_dir),
195
+ "--min-residues",
196
+ "100",
197
+ "--max-residues",
198
+ "200",
199
+ "--copy-method",
200
+ "symlink",
201
+ "--write-stats",
202
+ str(stats_fn),
203
+ ]
204
+
205
+ main(argv)
206
+
207
+ # Check output files
208
+ output_files = list(output_dir.iterdir())
209
+ assert len(output_files) == 1
210
+ expected_passed_file = output_dir / sample_cif.name
211
+ assert expected_passed_file in output_files
212
+
213
+ # Check stats file
214
+ with stats_fn.open() as f:
215
+ rows = list(csv.DictReader(f))
216
+ # Input files processed in alphabetical order
217
+ expected_stats = [
218
+ {
219
+ "input_file": str(local_sample2),
220
+ "residue_count": "8",
221
+ "passed": "False",
222
+ "output_file": "",
223
+ },
224
+ {
225
+ "input_file": str(local_sample),
226
+ "residue_count": "173",
227
+ "passed": "True",
228
+ "output_file": str(expected_passed_file),
229
+ },
230
+ ]
231
+ assert rows == expected_stats
232
+
233
+ # Check captured output
234
+ captured = capsys.readouterr()
235
+ assert "by number of residues in chain A" in captured.err
236
+ assert "Wrote 1 files to" in captured.err
237
+ assert "Statistics written to" in captured.err
238
+
239
+
240
+ def test_filter_secondary_structure(
241
+ sample_cif: Path, sample2_cif: Path, tmp_path: Path, capsys: pytest.CaptureFixture[str]
242
+ ):
243
+ input_dir = tmp_path / "input"
244
+ input_dir.mkdir()
245
+ local_sample = input_dir / sample_cif.name
246
+ local_sample.symlink_to(sample_cif)
247
+ local_sample2 = input_dir / sample2_cif.name
248
+ local_sample2.symlink_to(sample2_cif)
249
+ output_dir = tmp_path / "output"
250
+ output_dir.mkdir()
251
+ stats_fn = tmp_path / "ss_stats.csv"
252
+
253
+ argv = [
254
+ "filter",
255
+ "secondary-structure",
256
+ str(input_dir),
257
+ str(output_dir),
258
+ "--abs-min-helix-residues",
259
+ "10",
260
+ "--copy-method",
261
+ "symlink",
262
+ "--write-stats",
263
+ str(stats_fn),
264
+ ]
265
+
266
+ main(argv)
267
+
268
+ # Check output files
269
+ output_files = list(output_dir.iterdir())
270
+ assert len(output_files) == 1
271
+ expected_passed_file = output_dir / sample_cif.name
272
+ assert expected_passed_file in output_files
273
+
274
+ # Check stats file
275
+ with stats_fn.open() as f:
276
+ rows = list(csv.DictReader(f))
277
+ expected_stats = [
278
+ {
279
+ "helix_ratio": "0.0",
280
+ "input_file": str(local_sample2),
281
+ "nr_helix_residues": "0",
282
+ "nr_residues": "8",
283
+ "nr_sheet_residues": "0",
284
+ "output_file": "",
285
+ "passed": "False",
286
+ "sheet_ratio": "0.0",
287
+ },
288
+ {
289
+ "input_file": str(local_sample),
290
+ "nr_residues": "173",
291
+ "nr_helix_residues": "58",
292
+ "nr_sheet_residues": "59",
293
+ "helix_ratio": f"{58 / 173:.3f}",
294
+ "sheet_ratio": f"{59 / 173:.3f}",
295
+ "passed": "True",
296
+ "output_file": str(expected_passed_file),
297
+ },
298
+ ]
299
+ assert rows == expected_stats
300
+
301
+ # Check captured output
302
+ captured = capsys.readouterr()
303
+ assert "by secondary structure" in captured.err
304
+ assert "Wrote 1 files to" in captured.err
305
+ assert "Statistics written to" in captured.err
@@ -0,0 +1,78 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from protein_quest.filters import (
6
+ ChainFilterStatistics,
7
+ ResidueFilterStatistics,
8
+ filter_files_on_chain,
9
+ filter_files_on_residues,
10
+ )
11
+ from protein_quest.structure import ChainNotFoundError
12
+
13
+
14
+ @pytest.mark.parametrize(
15
+ "scheduler_address,expected_progress_bar",
16
+ [
17
+ (None, "Completed"), # creates a local cluster
18
+ ("sequential", "file/s"),
19
+ ],
20
+ )
21
+ def test_filter_files_on_chain_local_cluster(
22
+ sample2_cif: Path,
23
+ tmp_path: Path,
24
+ capsys: pytest.CaptureFixture[str],
25
+ scheduler_address: str | None,
26
+ expected_progress_bar: str,
27
+ ):
28
+ file2chains = [
29
+ (sample2_cif, "A"), # should pass
30
+ (sample2_cif, "B"), # should be discarded
31
+ ]
32
+
33
+ results = filter_files_on_chain(file2chains, tmp_path, scheduler_address=scheduler_address)
34
+
35
+ expected_passed = ChainFilterStatistics(
36
+ input_file=sample2_cif,
37
+ chain_id="A",
38
+ passed=True,
39
+ output_file=tmp_path / "2Y29_A2A.cif.gz",
40
+ )
41
+ assert expected_passed.output_file and expected_passed.output_file.exists()
42
+ expected_discarded = ChainFilterStatistics(
43
+ input_file=sample2_cif,
44
+ chain_id="B",
45
+ passed=False,
46
+ output_file=None,
47
+ discard_reason=ChainNotFoundError("B", sample2_cif, {"A"}),
48
+ )
49
+ assert results == [expected_passed, expected_discarded]
50
+
51
+ _, stderr = capsys.readouterr()
52
+ assert expected_progress_bar in stderr
53
+
54
+
55
+ def test_filter_files_on_residues(sample_cif: Path, sample2_cif: Path, tmp_path: Path):
56
+ results = list(
57
+ filter_files_on_residues(
58
+ input_files=[sample_cif, sample2_cif],
59
+ output_dir=tmp_path,
60
+ min_residues=100,
61
+ max_residues=200,
62
+ )
63
+ )
64
+ expected_passed = ResidueFilterStatistics(
65
+ input_file=sample_cif,
66
+ residue_count=173,
67
+ passed=True,
68
+ output_file=tmp_path / sample_cif.name,
69
+ )
70
+ assert expected_passed.output_file and expected_passed.output_file.exists()
71
+ expected_discarded = ResidueFilterStatistics(
72
+ input_file=sample2_cif,
73
+ residue_count=8,
74
+ passed=False,
75
+ output_file=None,
76
+ )
77
+
78
+ assert results == [expected_passed, expected_discarded]
@@ -1,101 +0,0 @@
1
- from pathlib import Path
2
- from textwrap import dedent
3
-
4
- import pytest
5
-
6
- from protein_quest.cli import main, make_parser
7
-
8
-
9
- def test_make_parser_help(capsys: pytest.CaptureFixture[str]):
10
- in_args = ["--help"]
11
- parser = make_parser()
12
- with pytest.raises(SystemExit):
13
- parser.parse_args(in_args)
14
-
15
- captured = capsys.readouterr()
16
- assert "Protein Quest CLI" in captured.out
17
-
18
-
19
- @pytest.mark.vcr
20
- def test_search_uniprot(capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture):
21
- argv = [
22
- "search",
23
- "uniprot",
24
- "--taxon-id",
25
- "9606",
26
- "--reviewed",
27
- "--limit",
28
- "1",
29
- "-",
30
- ]
31
-
32
- main(argv)
33
-
34
- captured = capsys.readouterr()
35
- expected = "A0A024R1R8\n"
36
- assert captured.out == expected
37
- assert "Searching for UniProt accessions" in captured.err
38
- assert "Found 1 UniProt accessions, written to <stdout>" in captured.err
39
- assert "There may be more results available" in caplog.text
40
-
41
-
42
- @pytest.mark.vcr
43
- def test_search_pdbe(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
44
- input_text = tmp_path / "uniprot_accessions.txt"
45
- input_text.write_text("P00811\n")
46
- output_file = tmp_path / "pdbe_results.csv"
47
- argv = [
48
- "search",
49
- "pdbe",
50
- "--limit",
51
- "150",
52
- "--min-residues",
53
- "360", # P00811 has 377 residues and 5 full PDB entries
54
- str(input_text),
55
- str(output_file),
56
- ]
57
-
58
- main(argv)
59
-
60
- result = output_file.read_text()
61
- expected = dedent("""\
62
- uniprot_accession,pdb_id,method,resolution,uniprot_chains,chain,chain_length
63
- P00811,9C6P,X-Ray_Crystallography,1.66,A/B=1-377,A,377
64
- P00811,9C81,X-Ray_Crystallography,1.7,A/B=1-377,A,377
65
- P00811,9C83,X-Ray_Crystallography,2.9,A/B=1-377,A,377
66
- P00811,9C84,X-Ray_Crystallography,1.7,A/B=1-377,A,377
67
- P00811,9DHL,X-Ray_Crystallography,1.88,A/B=1-377,A,377
68
- """)
69
- assert result == expected
70
-
71
- captured = capsys.readouterr()
72
- assert "Finding PDB entries for 1 uniprot accessions" in captured.err
73
- assert "Before filtering found 120 PDB entries for 1 uniprot accessions." in captured.err
74
- assert "After filtering on chain length (360, None) remained 5 PDB entries for 1 uniprot" in captured.err
75
- assert "Written to " in captured.err
76
-
77
-
78
- @pytest.mark.vcr
79
- def test_search_uniprot_details(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
80
- input_text = tmp_path / "uniprot_accessions.txt"
81
- input_text.write_text("P05067\nA0A0B5AC95\n")
82
- output_csv = tmp_path / "uniprot_details.csv"
83
- argv = [
84
- "search",
85
- "uniprot-details",
86
- str(input_text),
87
- str(output_csv),
88
- ]
89
-
90
- main(argv)
91
-
92
- result = output_csv.read_text()
93
- expected = dedent("""\
94
- uniprot_accession,uniprot_id,sequence_length,reviewed,protein_name,taxon_id,taxon_name
95
- A0A0B5AC95,INS1A_CONGE,115,True,Con-Ins G1a,6491,Conus geographus
96
- P05067,A4_HUMAN,770,True,Amyloid-beta precursor protein,9606,Homo sapiens
97
- """)
98
- assert result == expected
99
- captured = capsys.readouterr()
100
- assert "Retrieving UniProt entry details for 2 uniprot accessions" in captured.err
101
- assert "Retrieved details for 2 UniProt entries, written to " in captured.err
File without changes
File without changes