structpick 0.1.0__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.
- structpick-0.1.0/.github/workflows/publish.yml +30 -0
- structpick-0.1.0/.github/workflows/tests.yml +40 -0
- structpick-0.1.0/.gitignore +9 -0
- structpick-0.1.0/LICENSE +21 -0
- structpick-0.1.0/PKG-INFO +132 -0
- structpick-0.1.0/README.md +107 -0
- structpick-0.1.0/pyproject.toml +59 -0
- structpick-0.1.0/src/structpick/__init__.py +15 -0
- structpick-0.1.0/src/structpick/__main__.py +3 -0
- structpick-0.1.0/src/structpick/api.py +162 -0
- structpick-0.1.0/src/structpick/cli.py +186 -0
- structpick-0.1.0/src/structpick/models.py +70 -0
- structpick-0.1.0/src/structpick/py.typed +0 -0
- structpick-0.1.0/src/structpick/select.py +67 -0
- structpick-0.1.0/tests/__init__.py +0 -0
- structpick-0.1.0/tests/conftest.py +49 -0
- structpick-0.1.0/tests/fixtures/alphafold_P69905.json +15 -0
- structpick-0.1.0/tests/fixtures/best_structures_P69905.json +76 -0
- structpick-0.1.0/tests/fixtures/ligands_1bab.json +40 -0
- structpick-0.1.0/tests/fixtures/ligands_2dn2.json +28 -0
- structpick-0.1.0/tests/fixtures/uniprot_P69905.json +74 -0
- structpick-0.1.0/tests/test_api.py +72 -0
- structpick-0.1.0/tests/test_cli.py +85 -0
- structpick-0.1.0/tests/test_live.py +18 -0
- structpick-0.1.0/tests/test_select.py +68 -0
- structpick-0.1.0/uv.lock +845 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v6
|
|
13
|
+
- run: uv build
|
|
14
|
+
- uses: actions/upload-artifact@v4
|
|
15
|
+
with:
|
|
16
|
+
name: dist
|
|
17
|
+
path: dist/
|
|
18
|
+
|
|
19
|
+
pypi:
|
|
20
|
+
needs: build
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: pypi
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write # trusted publishing, no API token stored in the repo
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/download-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v6
|
|
14
|
+
- run: uv sync
|
|
15
|
+
- run: uv run ruff format --check .
|
|
16
|
+
- run: uv run ruff check .
|
|
17
|
+
- run: uv run mypy
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
strategy:
|
|
22
|
+
matrix:
|
|
23
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: astral-sh/setup-uv@v6
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
- run: uv sync
|
|
30
|
+
- run: uv run pytest --cov=structpick --cov-report=term-missing
|
|
31
|
+
|
|
32
|
+
live:
|
|
33
|
+
# The real APIs, once per run. Allowed to fail: EBI being down shouldn't block a PR.
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
continue-on-error: true
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: astral-sh/setup-uv@v6
|
|
39
|
+
- run: uv sync
|
|
40
|
+
- run: uv run pytest -m network
|
structpick-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luís Felipe de Oliveira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: structpick
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pick the best protein structure for docking: ranks PDB entries for a UniProt target, flags bound ligands and falls back to AlphaFold.
|
|
5
|
+
Project-URL: Homepage, https://github.com/felipsoliveira/structpick
|
|
6
|
+
Project-URL: Issues, https://github.com/felipsoliveira/structpick/issues
|
|
7
|
+
Author: Luís Felipe de Oliveira
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: alphafold,bioinformatics,docking,drug-discovery,pdb,structural-biology,uniprot
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# structpick
|
|
27
|
+
|
|
28
|
+
[](https://github.com/felipsoliveira/structpick/actions/workflows/tests.yml)
|
|
29
|
+
[](https://pypi.org/project/structpick/)
|
|
30
|
+
[](https://pypi.org/project/structpick/)
|
|
31
|
+
|
|
32
|
+
Pick a protein structure to dock against.
|
|
33
|
+
|
|
34
|
+
Given a UniProt accession, `structpick` lists the experimental structures from the PDB ranked by sequence coverage and resolution, shows which ones have a real ligand bound (ignoring glycerol, sulfate, ions and other crystallisation leftovers) and reports the AlphaFold model as a fallback. It's the first step of most structure-based drug design projects, and usually it means clicking through a dozen PDB pages.
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
$ structpick list Q27743 --holo --max-resolution 2.0 --top 5
|
|
38
|
+
Q27743: L-lactate dehydrogenase | Plasmodium falciparum (isolate CDC / Honduras) | 316 aa
|
|
39
|
+
rank pdb_id chain method resolution coverage residues ligands
|
|
40
|
+
1 4plz A X-ray diffraction 1.05 100% 1-316 NAI,OXM
|
|
41
|
+
2 1t2d A X-ray diffraction 1.10 100% 1-316 NAD,OXL
|
|
42
|
+
3 2a94 A X-ray diffraction 1.50 100% 2-316 AP0
|
|
43
|
+
4 1t24 A X-ray diffraction 1.70 100% 1-316 NAD,OXQ
|
|
44
|
+
5 1u4o A X-ray diffraction 1.70 100% 2-316 NDD
|
|
45
|
+
AlphaFold: AF-Q27743-F1 v6, mean pLDDT 97.4, 98% of residues very high confidence
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Looking for a structure of EGFR with erlotinib (`AQ4`) bound:
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
$ structpick list P00533 --method xray --ligand AQ4 --top 3
|
|
52
|
+
P00533 (EGFR): Epidermal growth factor receptor | Homo sapiens | 1210 aa
|
|
53
|
+
rank pdb_id chain method resolution coverage residues ligands
|
|
54
|
+
1 1m17 A X-ray diffraction 2.60 28% 695-1022 AQ4
|
|
55
|
+
2 4hjo A X-ray diffraction 2.75 28% 696-1022 AQ4
|
|
56
|
+
AlphaFold: AF-P00533-F1 v6, mean pLDDT 75.9, 47% of residues very high confidence
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install structpick
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Python 3.10 or newer. The only dependency is [httpx](https://www.python-httpx.org/).
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
structpick list <accession> [filters] [--top N] [--format table|csv|json]
|
|
71
|
+
structpick fetch <accession> [filters] [-o DIR] [--alphafold]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Option | Meaning |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `--max-resolution 2.0` | drop entries worse than 2.0 Å (also drops NMR, which has no resolution) |
|
|
77
|
+
| `--min-coverage 0.8` | keep chains covering at least 80% of the UniProt sequence |
|
|
78
|
+
| `--method xray` | `xray`, `em`, `nmr` or the full PDBe method name |
|
|
79
|
+
| `--ligand HEM` | only chains with this ligand bound |
|
|
80
|
+
| `--holo` | only chains with any ligand that isn't an additive or ion |
|
|
81
|
+
|
|
82
|
+
`fetch` downloads the best match as mmCIF from RCSB. If nothing matches the filters, or with `--alphafold`, it downloads the AlphaFold model instead.
|
|
83
|
+
|
|
84
|
+
`--format csv` and `--format json` are meant for scripts and notebooks:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
structpick list P00533 --holo --format csv > egfr_structures.csv
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### From Python
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from structpick import Filters, select_structures
|
|
94
|
+
from structpick.api import Client
|
|
95
|
+
|
|
96
|
+
with Client() as client:
|
|
97
|
+
structures = client.with_ligands(client.structures("P00533"))
|
|
98
|
+
best = select_structures(structures, Filters(max_resolution=2.5, require_ligand=True))
|
|
99
|
+
for s in best[:3]:
|
|
100
|
+
print(s.pdb_id, s.chain, s.resolution, [lig.code for lig in s.bound_ligands])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## How ranking works
|
|
104
|
+
|
|
105
|
+
1. **Coverage**, compared in 10% steps, so a chain covering 99% and one covering 100% count as equal.
|
|
106
|
+
2. **Resolution**, best first. Entries without a resolution (NMR) go last.
|
|
107
|
+
|
|
108
|
+
Ligands only count if they sit on the same chain and aren't in the [additive list](src/structpick/models.py) (water, glycerol, ethylene glycol, PEG, sulfate, phosphate, common buffers, ions, detergents). Metal ions are on that list too, so check catalytic metals yourself when they matter.
|
|
109
|
+
|
|
110
|
+
## Data sources
|
|
111
|
+
|
|
112
|
+
- [UniProt REST API](https://rest.uniprot.org) for the target name, gene and length
|
|
113
|
+
- [PDBe best structures](https://www.ebi.ac.uk/pdbe/api/doc/sifts.html) (SIFTS mappings) for chain coverage, method and resolution
|
|
114
|
+
- [PDBe ligand monomers](https://www.ebi.ac.uk/pdbe/api/doc/pdb.html) for bound ligands
|
|
115
|
+
- [AlphaFold Protein Structure Database](https://alphafold.ebi.ac.uk/api-docs) for predicted models
|
|
116
|
+
|
|
117
|
+
Requests time out after 30 s and are retried with exponential backoff on 429 and 5xx responses. Ligands are only looked up for the chains left after the other filters, one request per PDB entry.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git clone https://github.com/felipsoliveira/structpick
|
|
123
|
+
cd structpick
|
|
124
|
+
uv sync # or: pip install -e . pytest pytest-cov ruff mypy
|
|
125
|
+
uv run pytest # offline, uses recorded API responses in tests/fixtures
|
|
126
|
+
uv run pytest -m network # hits the real APIs
|
|
127
|
+
uv run ruff check . && uv run mypy
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# structpick
|
|
2
|
+
|
|
3
|
+
[](https://github.com/felipsoliveira/structpick/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/structpick/)
|
|
5
|
+
[](https://pypi.org/project/structpick/)
|
|
6
|
+
|
|
7
|
+
Pick a protein structure to dock against.
|
|
8
|
+
|
|
9
|
+
Given a UniProt accession, `structpick` lists the experimental structures from the PDB ranked by sequence coverage and resolution, shows which ones have a real ligand bound (ignoring glycerol, sulfate, ions and other crystallisation leftovers) and reports the AlphaFold model as a fallback. It's the first step of most structure-based drug design projects, and usually it means clicking through a dozen PDB pages.
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
$ structpick list Q27743 --holo --max-resolution 2.0 --top 5
|
|
13
|
+
Q27743: L-lactate dehydrogenase | Plasmodium falciparum (isolate CDC / Honduras) | 316 aa
|
|
14
|
+
rank pdb_id chain method resolution coverage residues ligands
|
|
15
|
+
1 4plz A X-ray diffraction 1.05 100% 1-316 NAI,OXM
|
|
16
|
+
2 1t2d A X-ray diffraction 1.10 100% 1-316 NAD,OXL
|
|
17
|
+
3 2a94 A X-ray diffraction 1.50 100% 2-316 AP0
|
|
18
|
+
4 1t24 A X-ray diffraction 1.70 100% 1-316 NAD,OXQ
|
|
19
|
+
5 1u4o A X-ray diffraction 1.70 100% 2-316 NDD
|
|
20
|
+
AlphaFold: AF-Q27743-F1 v6, mean pLDDT 97.4, 98% of residues very high confidence
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Looking for a structure of EGFR with erlotinib (`AQ4`) bound:
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ structpick list P00533 --method xray --ligand AQ4 --top 3
|
|
27
|
+
P00533 (EGFR): Epidermal growth factor receptor | Homo sapiens | 1210 aa
|
|
28
|
+
rank pdb_id chain method resolution coverage residues ligands
|
|
29
|
+
1 1m17 A X-ray diffraction 2.60 28% 695-1022 AQ4
|
|
30
|
+
2 4hjo A X-ray diffraction 2.75 28% 696-1022 AQ4
|
|
31
|
+
AlphaFold: AF-P00533-F1 v6, mean pLDDT 75.9, 47% of residues very high confidence
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install structpick
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Python 3.10 or newer. The only dependency is [httpx](https://www.python-httpx.org/).
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
structpick list <accession> [filters] [--top N] [--format table|csv|json]
|
|
46
|
+
structpick fetch <accession> [filters] [-o DIR] [--alphafold]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Option | Meaning |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `--max-resolution 2.0` | drop entries worse than 2.0 Å (also drops NMR, which has no resolution) |
|
|
52
|
+
| `--min-coverage 0.8` | keep chains covering at least 80% of the UniProt sequence |
|
|
53
|
+
| `--method xray` | `xray`, `em`, `nmr` or the full PDBe method name |
|
|
54
|
+
| `--ligand HEM` | only chains with this ligand bound |
|
|
55
|
+
| `--holo` | only chains with any ligand that isn't an additive or ion |
|
|
56
|
+
|
|
57
|
+
`fetch` downloads the best match as mmCIF from RCSB. If nothing matches the filters, or with `--alphafold`, it downloads the AlphaFold model instead.
|
|
58
|
+
|
|
59
|
+
`--format csv` and `--format json` are meant for scripts and notebooks:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
structpick list P00533 --holo --format csv > egfr_structures.csv
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### From Python
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from structpick import Filters, select_structures
|
|
69
|
+
from structpick.api import Client
|
|
70
|
+
|
|
71
|
+
with Client() as client:
|
|
72
|
+
structures = client.with_ligands(client.structures("P00533"))
|
|
73
|
+
best = select_structures(structures, Filters(max_resolution=2.5, require_ligand=True))
|
|
74
|
+
for s in best[:3]:
|
|
75
|
+
print(s.pdb_id, s.chain, s.resolution, [lig.code for lig in s.bound_ligands])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## How ranking works
|
|
79
|
+
|
|
80
|
+
1. **Coverage**, compared in 10% steps, so a chain covering 99% and one covering 100% count as equal.
|
|
81
|
+
2. **Resolution**, best first. Entries without a resolution (NMR) go last.
|
|
82
|
+
|
|
83
|
+
Ligands only count if they sit on the same chain and aren't in the [additive list](src/structpick/models.py) (water, glycerol, ethylene glycol, PEG, sulfate, phosphate, common buffers, ions, detergents). Metal ions are on that list too, so check catalytic metals yourself when they matter.
|
|
84
|
+
|
|
85
|
+
## Data sources
|
|
86
|
+
|
|
87
|
+
- [UniProt REST API](https://rest.uniprot.org) for the target name, gene and length
|
|
88
|
+
- [PDBe best structures](https://www.ebi.ac.uk/pdbe/api/doc/sifts.html) (SIFTS mappings) for chain coverage, method and resolution
|
|
89
|
+
- [PDBe ligand monomers](https://www.ebi.ac.uk/pdbe/api/doc/pdb.html) for bound ligands
|
|
90
|
+
- [AlphaFold Protein Structure Database](https://alphafold.ebi.ac.uk/api-docs) for predicted models
|
|
91
|
+
|
|
92
|
+
Requests time out after 30 s and are retried with exponential backoff on 429 and 5xx responses. Ligands are only looked up for the chains left after the other filters, one request per PDB entry.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/felipsoliveira/structpick
|
|
98
|
+
cd structpick
|
|
99
|
+
uv sync # or: pip install -e . pytest pytest-cov ruff mypy
|
|
100
|
+
uv run pytest # offline, uses recorded API responses in tests/fixtures
|
|
101
|
+
uv run pytest -m network # hits the real APIs
|
|
102
|
+
uv run ruff check . && uv run mypy
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "structpick"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pick the best protein structure for docking: ranks PDB entries for a UniProt target, flags bound ligands and falls back to AlphaFold."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Luís Felipe de Oliveira" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = ["httpx>=0.27"]
|
|
15
|
+
keywords = ["bioinformatics", "structural-biology", "docking", "pdb", "alphafold", "uniprot", "drug-discovery"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/felipsoliveira/structpick"
|
|
32
|
+
Issues = "https://github.com/felipsoliveira/structpick/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
structpick = "structpick.cli:main"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "mypy>=1.11"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py310"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
strict = true
|
|
49
|
+
files = ["src", "tests"]
|
|
50
|
+
|
|
51
|
+
[[tool.mypy.overrides]]
|
|
52
|
+
module = "tests.*"
|
|
53
|
+
disallow_untyped_defs = false
|
|
54
|
+
disallow_untyped_calls = false
|
|
55
|
+
disallow_incomplete_defs = false
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
addopts = "-m 'not network'"
|
|
59
|
+
markers = ["network: hits the real EBI/UniProt APIs (deselected by default)"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Rank experimental and predicted structures of a protein target for docking."""
|
|
2
|
+
|
|
3
|
+
from structpick.models import AlphaFoldModel, Ligand, Structure, Target, is_additive
|
|
4
|
+
from structpick.select import Filters, select_structures
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AlphaFoldModel",
|
|
8
|
+
"Filters",
|
|
9
|
+
"Ligand",
|
|
10
|
+
"Structure",
|
|
11
|
+
"Target",
|
|
12
|
+
"is_additive",
|
|
13
|
+
"select_structures",
|
|
14
|
+
]
|
|
15
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from structpick import __version__
|
|
10
|
+
from structpick.models import AlphaFoldModel, Ligand, Structure, Target
|
|
11
|
+
|
|
12
|
+
PDBE = "https://www.ebi.ac.uk/pdbe/api"
|
|
13
|
+
ALPHAFOLD = "https://alphafold.ebi.ac.uk/api"
|
|
14
|
+
UNIPROT = "https://rest.uniprot.org"
|
|
15
|
+
|
|
16
|
+
RETRY_STATUS = {429, 500, 502, 503, 504}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TargetNotFoundError(LookupError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Client:
|
|
24
|
+
"""Small wrapper around the UniProt, PDBe and AlphaFold DB REST APIs."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
http: httpx.Client | None = None,
|
|
29
|
+
retries: int = 3,
|
|
30
|
+
backoff: float = 1.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._http = http or httpx.Client(
|
|
33
|
+
timeout=httpx.Timeout(30.0),
|
|
34
|
+
headers={"User-Agent": f"structpick/{__version__}"},
|
|
35
|
+
follow_redirects=True,
|
|
36
|
+
)
|
|
37
|
+
self._retries = retries
|
|
38
|
+
self._backoff = backoff
|
|
39
|
+
|
|
40
|
+
def __enter__(self) -> Client:
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def __exit__(self, *exc: object) -> None:
|
|
44
|
+
self.close()
|
|
45
|
+
|
|
46
|
+
def close(self) -> None:
|
|
47
|
+
self._http.close()
|
|
48
|
+
|
|
49
|
+
def _get(
|
|
50
|
+
self,
|
|
51
|
+
url: str,
|
|
52
|
+
params: dict[str, str] | None = None,
|
|
53
|
+
missing: frozenset[int] = frozenset({404}),
|
|
54
|
+
) -> Any | None:
|
|
55
|
+
"""GET JSON. Returns None for `missing` statuses; retries rate limits and server errors."""
|
|
56
|
+
for attempt in range(self._retries + 1):
|
|
57
|
+
try:
|
|
58
|
+
response = self._http.get(url, params=params)
|
|
59
|
+
except httpx.TransportError:
|
|
60
|
+
if attempt == self._retries:
|
|
61
|
+
raise
|
|
62
|
+
else:
|
|
63
|
+
if response.status_code in missing:
|
|
64
|
+
return None
|
|
65
|
+
if response.status_code not in RETRY_STATUS or attempt == self._retries:
|
|
66
|
+
response.raise_for_status()
|
|
67
|
+
return response.json()
|
|
68
|
+
time.sleep(self._backoff * 2**attempt)
|
|
69
|
+
raise AssertionError("unreachable")
|
|
70
|
+
|
|
71
|
+
def target(self, accession: str) -> Target:
|
|
72
|
+
data = self._get(
|
|
73
|
+
f"{UNIPROT}/uniprotkb/{accession}",
|
|
74
|
+
params={
|
|
75
|
+
"fields": "accession,protein_name,gene_names,organism_name,length",
|
|
76
|
+
"format": "json",
|
|
77
|
+
},
|
|
78
|
+
# UniProt answers 400 for strings that aren't valid accessions.
|
|
79
|
+
missing=frozenset({400, 404}),
|
|
80
|
+
)
|
|
81
|
+
if data is None:
|
|
82
|
+
raise TargetNotFoundError(f"UniProt has no entry {accession!r}")
|
|
83
|
+
description: dict[str, Any] = data.get("proteinDescription", {})
|
|
84
|
+
submissions: list[dict[str, Any]] = description.get("submissionNames", [])
|
|
85
|
+
name = (
|
|
86
|
+
description.get("recommendedName", {}).get("fullName", {}).get("value")
|
|
87
|
+
or (submissions[0].get("fullName", {}).get("value") if submissions else None)
|
|
88
|
+
or accession
|
|
89
|
+
)
|
|
90
|
+
genes = data.get("genes") or []
|
|
91
|
+
gene = genes[0].get("geneName", {}).get("value") if genes else None
|
|
92
|
+
return Target(
|
|
93
|
+
accession=data["primaryAccession"],
|
|
94
|
+
name=name,
|
|
95
|
+
gene=gene,
|
|
96
|
+
organism=data.get("organism", {}).get("scientificName", ""),
|
|
97
|
+
length=data.get("sequence", {}).get("length", 0),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def structures(self, accession: str) -> list[Structure]:
|
|
101
|
+
data = self._get(f"{PDBE}/mappings/best_structures/{accession}")
|
|
102
|
+
if not data:
|
|
103
|
+
return []
|
|
104
|
+
rows: list[dict[str, Any]] = data.get(accession) or next(iter(data.values()), [])
|
|
105
|
+
return [
|
|
106
|
+
Structure(
|
|
107
|
+
pdb_id=row["pdb_id"],
|
|
108
|
+
chain=row["chain_id"],
|
|
109
|
+
method=row.get("experimental_method", ""),
|
|
110
|
+
resolution=row.get("resolution"),
|
|
111
|
+
coverage=float(row.get("coverage", 0.0)),
|
|
112
|
+
unp_start=row.get("unp_start", 0),
|
|
113
|
+
unp_end=row.get("unp_end", 0),
|
|
114
|
+
)
|
|
115
|
+
for row in rows
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
def ligands(self, pdb_id: str) -> tuple[Ligand, ...]:
|
|
119
|
+
data = self._get(f"{PDBE}/pdb/entry/ligand_monomers/{pdb_id}")
|
|
120
|
+
if not data:
|
|
121
|
+
return ()
|
|
122
|
+
return tuple(
|
|
123
|
+
Ligand(
|
|
124
|
+
code=row["chem_comp_id"], name=row.get("chem_comp_name", ""), chain=row["chain_id"]
|
|
125
|
+
)
|
|
126
|
+
for row in data.get(pdb_id, [])
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def with_ligands(self, structures: list[Structure]) -> list[Structure]:
|
|
130
|
+
"""Attach ligands, fetching each PDB entry once."""
|
|
131
|
+
cache: dict[str, tuple[Ligand, ...]] = {}
|
|
132
|
+
result = []
|
|
133
|
+
for structure in structures:
|
|
134
|
+
if structure.pdb_id not in cache:
|
|
135
|
+
cache[structure.pdb_id] = self.ligands(structure.pdb_id)
|
|
136
|
+
result.append(replace(structure, ligands=cache[structure.pdb_id]))
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
def alphafold(self, accession: str) -> AlphaFoldModel | None:
|
|
140
|
+
data = self._get(f"{ALPHAFOLD}/prediction/{accession}")
|
|
141
|
+
if not data:
|
|
142
|
+
return None
|
|
143
|
+
row = next((r for r in data if r.get("uniprotAccession") == accession), data[0])
|
|
144
|
+
return AlphaFoldModel(
|
|
145
|
+
accession=row["uniprotAccession"],
|
|
146
|
+
version=row.get("latestVersion", 0),
|
|
147
|
+
mean_plddt=row.get("globalMetricValue", 0.0),
|
|
148
|
+
fraction_very_high=row.get("fractionPlddtVeryHigh", 0.0),
|
|
149
|
+
start=row.get("sequenceStart", 0),
|
|
150
|
+
end=row.get("sequenceEnd", 0),
|
|
151
|
+
cif_url=row["cifUrl"],
|
|
152
|
+
pdb_url=row["pdbUrl"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def download(self, url: str) -> bytes:
|
|
156
|
+
response = self._http.get(url)
|
|
157
|
+
response.raise_for_status()
|
|
158
|
+
return response.content
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def entry_url(pdb_id: str, fmt: str = "cif") -> str:
|
|
162
|
+
return f"https://files.rcsb.org/download/{pdb_id.upper()}.{fmt}"
|