Skip to content

Commit 2257de2

Browse files
brunokamardcoreMarekMichał Krutul
authored
Integrate with API v2 (#6)
* Adapt python wrapper to API v2 * Add InvalidTargetStrategy support * Update setup.py * Readme update * Update API docs link * Improved readme, version bump * Update README.md Co-authored-by: Mateusz Bruno-Kamiński <bruno@molecule.one> * Update README.md Co-authored-by: Mateusz Bruno-Kamiński <bruno@molecule.one> * Update README.md Co-authored-by: Mateusz Bruno-Kamiński <bruno@molecule.one> * Update README.md Co-authored-by: Mateusz Bruno-Kamiński <bruno@molecule.one> Co-authored-by: Szymon Pilkowski <s.pilkowski@molecule.one> Co-authored-by: Marek <marek@devalchemist.com> Co-authored-by: Michał Krutul <m.krutul@molecule.one>
1 parent 063309c commit 2257de2

9 files changed

Lines changed: 229 additions & 55 deletions

File tree

README.md

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,47 @@ pip install git+https://github.com/molecule-one/m1wrapper-python
1212
from m1wrapper import MoleculeOneWrapper
1313
m1wrapper = MoleculeOneWrapper(api_token, 'https://app.molecule.one')
1414
```
15-
- *api_token*: API token you'll need to authorize in our system. You can get
15+
- *api_token*: API token you'll need to authorize in our system. You can
1616
generate yours at https://app.molecule.one/dashboard/user/api-tokens
1717
- *api_base_url* (optional): URI of the batch scoring service. Defaults to Molecule One's public
1818
server, but you will need to provide custom value if you're using a dedicated solution.
1919

20-
### Running batch scoring request:
20+
### Getting a list of batch scoring requests:
21+
```py
22+
searches = m1wrapper.list_batch_searches()
23+
```
24+
25+
### Running new batch scoring request:
2126
```py
2227
search = m1wrapper.run_batch_search(
2328
targets=['cc', 'O=C(Nc1cc(Nc2nc(-c3cnccc3)ccn2)c(cc1)C)c3ccc(cc3)CN3CCN(CC3)C'],
24-
parameters={'exploratory_search': False, 'detail_level': 'score'}
29+
parameters={'model': 'gat'}
2530
)
2631
```
2732
- *targets*: list of target compounds in SMILES format
2833
- *parameters* (optional): additional configuration for your batch
29-
scoring request. See [Batch Scoring API](https://github.com/molecule-one/api/blob/master/batch-scoring.md) for more information.
30-
- *priority* (optional): priority of the batch request.
34+
scoring request. See [Batch Scoring API](https://github.com/molecule-one/api/blob/master/api-v2.md) for more information.
35+
- *detail_level* (optional): [detail level of the batch request](#batch-scoring-detail-level)
36+
- *priority* (optional): [priority of the batch request](#batch-scoring-priorities)
37+
- *invalid_target_strategy* (optional): if set to `InvalidTargetStrategy.PASS`, targets that cannot be canonized by our SMILES parser won't cause the whole batch request to be rejected. Defaults to `InvalidTargetStrategy.REJECT`.
3138
- *starting_materials* (optional): list of available compounds in SMILES format
3239

33-
### Batch scoring priorities:
40+
### Batch scoring detail level
41+
Detail level determines how much information about each target synthesis you'll get. We define it as a `DetailLevel` enum with two variants:
42+
- `DetailLevel.SCORE` (default) - useful when you're not interested in full synthesis json/UI preview, but only numerical values
43+
- `DetailLevel.SYNTHESIS` - when you're also interested in reactions and compounds leading to the target product
44+
#### Example:
45+
```py
46+
from m1wrapper import MoleculeOneWrapper, DetailLevel
47+
m1wrapper = MoleculeOneWrapper(api_token, 'https://app.molecule.one')
48+
search = m1wrapper.run_batch_search(
49+
targets=['cc', 'O=C(Nc1cc(Nc2nc(-c3cnccc3)ccn2)c(cc1)C)c3ccc(cc3)CN3CCN(CC3)C'],
50+
parameters={'model': 'gat', },
51+
detail_level=DetailLevel.SCORE
52+
)
53+
```
54+
55+
### Batch scoring priorities
3456
Priorities are defined as integers in a range of 1 to 10. Requests with higher priority will be processed before those with lower priority.
3557
For convenience, we also define a `Priority` enum with the following variants:
3658
- `Priority.LOWEST` (1)
@@ -45,8 +67,9 @@ from m1wrapper import MoleculeOneWrapper, Priority
4567
m1wrapper = MoleculeOneWrapper(api_token, 'https://app.molecule.one')
4668
search = m1wrapper.run_batch_search(
4769
targets=['cc', 'O=C(Nc1cc(Nc2nc(-c3cnccc3)ccn2)c(cc1)C)c3ccc(cc3)CN3CCN(CC3)C'],
48-
parameters={'exploratory_search': False, 'detail_level': 'score'},
49-
priority=Priority.HIGH)
70+
parameters={'model': 'gat'},
71+
priority=Priority.HIGH
72+
)
5073
```
5174

5275
### Getting exisiting scoring request by id:
@@ -71,32 +94,53 @@ Results are made available as soon as they are processed. This method
7194
provided a way to start working with some of your results without waiting until all targets are processed.
7295
This usually means implementing some kind of polling/scheduling on your side.
7396
```py
74-
results = search.get_partial_results(precision=5, only=["targetSmiles, "result"])
97+
results = search.get_partial_results(precision=5, only=["target_smiles", "result"])
7598
```
7699
- *precision* (optional): format the floating point scores returned by the system (certainty, result, price) to given number of significant digits.
77100
- *only* (optional): fetch only a subset of values. Defaults to
78101
all values.
79102

103+
Returns JSON object of the following shape:
104+
Returns an object of the following shape:
105+
```python
106+
[
107+
{
108+
'target_smiles': 'Cc1ccc(cc1Nc2nccc(n2)c3cccnc3)NC(=O)c4ccc(cc4)CN5CCN(CC5)C',
109+
'result': '7.53000'
110+
},
111+
...
112+
]
113+
```
114+
#### All values:
115+
```py
116+
results = search.get_partial_results(precision=5)
117+
```
118+
80119
Returns JSON object of the following shape:
81120
```json
82121
[
83122
{
84-
"targetSmiles": "Cc1ccc(cc1Nc2nccc(n2)c3cccnc3)NC(=O)c4ccc(cc4)CN5CCN(CC5)C",
85-
"status": "ok",
86-
"result": "7.53",
87-
"certainty": "0.581",
88-
"price": "5230",
89-
"reactionCount": 5,
90-
"timedOut": false
123+
'target_smiles': 'Cc1ccc(cc1Nc2nccc(n2)c3cccnc3)NC(=O)c4ccc(cc4)CN5CCN(CC5)C',
124+
'status': 'ok',
125+
'result': '7.53000',
126+
'certainty': '0.581',
127+
'price': '5230',
128+
'reaction_count': 5,
129+
'timed_out': False,
130+
'started_at': '2021-09-13T14:45:31.012Z',
131+
'finished_at': '2021-09-13T14:46:39.199Z',
132+
'running_time': 68.187,
133+
'url': 'https://app.molecule.one/dashboard/synthesis-plans/batch/b787bf5f-6736-443c-bef1-8f10a37da246/result/0e3c6e13-fce1-46ba-9811-8fe66e0e4122'
91134
},
92-
...
135+
...
93136
]
94137
```
138+
95139
See [Batch Scoring API](https://github.com/molecule-one/api/blob/master/batch-scoring.md) for a full explaination of returned fields.
96140

97141
### Getting complete results:
98142
```py
99-
results = search.get_results(precision=5, only=["targetSmiles, "result"])
143+
results = search.get_results(precision=5, only=["target_smiles", "result"])
100144
```
101145
If you don't want to implement scheduling on your own, this method
102146
provides a simple way to wait until all targets are processed (sending periodical checks using

examples/example.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
from m1wrapper import MoleculeOneWrapper
1+
from m1wrapper import MoleculeOneWrapper, Priority, DetailLevel
22

33
if __name__ == '__main__':
44
# get your token at https://app.molecule.one/dashboard/user/api-tokens
55
token = 'f4614b1d96124d09ab14fbe6537c9007_4ea55651a3904037b9fe4c4a72d2b85d'
66

77
m1wrapper = MoleculeOneWrapper(token)
88

9+
searches = m1wrapper.list_batch_searches()
10+
print('previous searches:', searches)
11+
912
search = m1wrapper.run_batch_search(
10-
targets=['cc', 'O=C(Nc1cc(Nc2nc(-c3cnccc3)ccn2)c(cc1)C)c3ccc(cc3)CN3CCN(CC3)C'],
11-
parameters={'exploratory_search': False, 'detail_level': 'score'}
13+
targets=[
14+
'cc', 'O=C(Nc1cc(Nc2nc(-c3cnccc3)ccn2)c(cc1)C)c3ccc(cc3)CN3CCN(CC3)C'],
15+
parameters={'model': 'gat'},
16+
detail_level=DetailLevel.SCORE,
17+
priority=Priority.LOW
1218
)
1319
print('created search:', search.search_id)
1420

@@ -24,7 +30,8 @@
2430
partial_results = search.get_partial_results()
2531
print("partial results:", partial_results)
2632

27-
results = search.get_results(precision=4, only=['targetSmiles', 'price', 'result'])
33+
results = search.get_results(
34+
precision=4, only=['target_smiles', 'price', 'result'])
2835
print('results:', results)
2936

3037
m1wrapper.delete_batch_search(search.search_id)

m1wrapper/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .m1wrapper import MoleculeOneWrapper, Priority
1+
from .m1wrapper import MoleculeOneWrapper, Priority, DetailLevel, InvalidTargetStrategy
2+
from .traverse import traverse_modify

m1wrapper/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
api_token_version = 'v1'
33
api_base_url = 'https://app.molecule.one/'
44
api_search_endpoint = 'batch-search'
5+
api_status_endpoint = 'batch-search-status'
56
api_results_endpoint = 'batch-search-result'
67
status_check_delay_s = 15
78
http_backoff_factor = 5

m1wrapper/errors.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import requests
2+
3+
def format_error_message(error):
4+
if error["message"] and error["errors"]:
5+
return f'{error["message"]}: {repr(error["errors"])}'
6+
if error["message"]:
7+
return f'{error["message"]}'
8+
else:
9+
return "unknown error"
10+
11+
12+
def maybe_handle_error(response):
13+
if response.status_code >= 400 and response.status_code < 500:
14+
error = format_error_message(response.json())
15+
raise requests.exceptions.HTTPError(error)
16+
else:
17+
response.raise_for_status()
18+

m1wrapper/m1wrapper.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import requests
2+
from urllib.parse import urljoin
13
from typing import List, Dict
2-
from enum import IntEnum
4+
from enum import Enum, IntEnum
35

46
from .search import BatchSearch
5-
from .config import api_token_version, wrapper_version, api_base_url
7+
from .config import (
8+
api_token_version,
9+
wrapper_version,
10+
api_base_url,
11+
api_search_endpoint
12+
)
13+
14+
from .errors import (
15+
maybe_handle_error
16+
)
617

718
class Priority(IntEnum):
819
LOWEST = 1,
@@ -11,6 +22,14 @@ class Priority(IntEnum):
1122
HIGH = 8,
1223
HIGHEST = 10
1324

25+
class DetailLevel(str, Enum):
26+
SCORE = 'score',
27+
SYNTHESIS = 'synthesis'
28+
29+
class InvalidTargetStrategy(str, Enum):
30+
REJECT = 'reject',
31+
PASS = 'pass'
32+
1433
class MoleculeOneWrapper:
1534
"""
1635
Wrapper for MoleculeOne Batch Scoring REST API
@@ -22,7 +41,7 @@ def __init__(
2241
api_base_url: str = api_base_url
2342
):
2443
self.api_token = api_token
25-
self.api_base_url = f'{api_base_url}/api/v1/'
44+
self.api_base_url = f'{api_base_url}/api/v2/'
2645
self.request_headers = self.__prepare_request_headers()
2746

2847
def __prepare_request_headers(self) -> dict:
@@ -31,29 +50,44 @@ def __prepare_request_headers(self) -> dict:
3150
'User-Agent': f'api-wrapper-python/{wrapper_version}',
3251
'Authorization': f'ApiToken-{api_token_version} {self.api_token}'
3352
}
53+
54+
def list_batch_searches(self):
55+
response = requests.get(
56+
urljoin(self.api_base_url, api_search_endpoint),
57+
headers=self.request_headers,
58+
)
59+
print(response)
60+
maybe_handle_error(response)
61+
return response.json()
3462

3563
def run_batch_search(
3664
self,
3765
targets: List[str],
3866
parameters: Dict = None,
67+
detail_level = DetailLevel.SCORE,
3968
priority = Priority.NORMAL,
69+
invalid_target_strategy = InvalidTargetStrategy.REJECT ,
4070
starting_materials: List[str] = None,
4171
) -> BatchSearch:
4272
return BatchSearch(
4373
self.api_base_url,
4474
self.request_headers,
4575
targets=targets,
4676
parameters=parameters,
77+
detail_level=detail_level,
4778
priority=int(priority),
79+
invalid_target_strategy=invalid_target_strategy,
4880
starting_materials=starting_materials
4981
)
5082

5183
def get_batch_search(self, search_id: str) -> BatchSearch:
52-
return BatchSearch.from_id(
84+
search = BatchSearch.from_id(
5385
self.api_base_url,
5486
self.request_headers,
5587
search_id
5688
)
89+
data = search.get()
90+
return search
5791

5892
def delete_batch_search(self, search_id: str):
5993
search = BatchSearch.from_id(

0 commit comments

Comments
 (0)