Skip to content

Commit 5464f17

Browse files
committed
Initial commit
0 parents  commit 5464f17

11 files changed

Lines changed: 408 additions & 0 deletions

File tree

.gitignore

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*,cover
46+
.hypothesis/
47+
48+
# Translations
49+
*.mo
50+
*.pot
51+
52+
# Django stuff:
53+
*.log
54+
local_settings.py
55+
56+
# Flask stuff:
57+
instance/
58+
.webassets-cache
59+
60+
# Scrapy stuff:
61+
.scrapy
62+
63+
# Sphinx documentation
64+
docs/_build/
65+
66+
# PyBuilder
67+
target/
68+
69+
# IPython Notebook
70+
.ipynb_checkpoints
71+
72+
# pyenv
73+
.python-version
74+
75+
# celery beat schedule file
76+
celerybeat-schedule
77+
78+
# dotenv
79+
.env
80+
81+
# virtualenv
82+
.venv/
83+
venv/
84+
ENV/
85+
86+
# Spyder project settings
87+
.spyderproject
88+
89+
# Rope project settings
90+
.ropeproject

LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# todo: license

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Molecule One Batch Scoring API Wrapper
2+
3+
## Usage:
4+
5+
### Installation:
6+
7+
```
8+
pip install git+https://github.com/molecule-one/m1wrapper-python
9+
```
10+
NOTE: make sure to install package to the intended python environment.
11+
12+
### Initialization:
13+
```py
14+
from m1wrapper import MoleculeOneWrapper
15+
m1wrapper = MoleculeOneWrapper(token)
16+
```
17+
- *token*: API token you'll need to authorize in our system. You can get
18+
generate yours at https://app.molecule.one/dashboard/user/api-tokens
19+
- *baseUrl* (optional): URI of the batch scoring service. Defaults to Molecule One's public
20+
server, but you will need to provide custom value if you're using a dedicated solution.
21+
22+
### Running batch scoring request:
23+
24+
```py
25+
search = m1wrapper.run_batch_search(
26+
targets=['cc', 'O=C(Nc1cc(Nc2nc(-c3cnccc3)ccn2)c(cc1)C)c3ccc(cc3)CN3CCN(CC3)C'],
27+
parameters={'exploratory_search': False, 'detail_level': 'score'}
28+
)
29+
```
30+
- *targets*: list of target compounds in SMILES format
31+
- *parameters* (optional): additional configuration for your batch
32+
scoring request. See [Batch Scoring API](https://github.com/molecule-one/api/blob/master/batch-scoring.md) for more information.
33+
34+
35+
### Getting exisiting scoring request by id:
36+
```py
37+
search = m1wrapper.get_batch_search(id)
38+
```
39+
40+
### Checking if your scoring request processing is finished:
41+
```py
42+
search.is_finished()
43+
```
44+
45+
### Checking full search status:
46+
```py
47+
status = search.get_status()
48+
```
49+
In response, you’ll get information about your batch scoring processing progress, i.e.:
50+
`{"queued":92,"running":4,"finished":104,"error":0}`
51+
52+
### Getting partial results:
53+
Results are made available as soon as they are processed. This method
54+
provided a way to start working with some of your results without waiting until all targets are processed.
55+
This usually means implementing some kind of polling/scheduling on your side.
56+
```py
57+
results = search.get_partial_results(precision=5, only=["targetSmiles, "result"])
58+
```
59+
- *precision* (optional): format the floating point scores returned by the system (certainty, result, price) to given number of significant digits.
60+
- *only* (optional): fetch only a subset of values. Defaults to
61+
all values.
62+
63+
Returns JSON object of the following shape:
64+
```json
65+
[
66+
{
67+
"targetSmiles": "Cc1ccc(cc1Nc2nccc(n2)c3cccnc3)NC(=O)c4ccc(cc4)CN5CCN(CC5)C",
68+
"status": "ok",
69+
"result": "7.53",
70+
"certainty": "0.581",
71+
"price": "5230",
72+
"reactionCount": 5,
73+
"timedOut": false
74+
},
75+
...
76+
]
77+
```
78+
See [Batch Scoring API](https://github.com/molecule-one/api/blob/master/batch-scoring.md) for a full explaination of returned fields.
79+
80+
### Getting complete results:
81+
```py
82+
results = search.get_results(precision=5, only=["targetSmiles, "result"])
83+
```
84+
If you don't want to implement scheduling on your own, this method
85+
provides a simple way to wait until all targets are processed (sending periodical checks using
86+
`search.is_finished()`), and execute only when all results are available. It's a
87+
blocking operation.
88+
Parameters and returned JSON are the same as with `get_partial_results()`.
89+
90+
### Deleting your data:
91+
```py
92+
m1wrapper.delete_batch_search(search.search_id)
93+
```

examples/__init__.py

Whitespace-only changes.

examples/example.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from m1wrapper import MoleculeOneWrapper
2+
3+
if __name__ == '__main__':
4+
# get your token at https://app.molecule.one/dashboard/user/api-tokens
5+
token = 'f4614b1d96124d09ab14fbe6537c9007_4ea55651a3904037b9fe4c4a72d2b85d'
6+
7+
m1wrapper = MoleculeOneWrapper(token)
8+
9+
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'}
12+
)
13+
print('created search:', search.search_id)
14+
15+
search = m1wrapper.get_batch_search(search.search_id)
16+
print('got search:', search.search_id)
17+
18+
status = search.get_status()
19+
print('status:', status)
20+
21+
is_finished = search.is_finished()
22+
print('is finished:', is_finished)
23+
24+
partial_results = search.get_partial_results()
25+
print("partial results:", partial_results)
26+
27+
results = search.get_results(precision=4, only=['targetSmiles', 'price', 'result'])
28+
print('results:', results)
29+
30+
m1wrapper.delete_batch_search(search.search_id)

m1wrapper/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .m1wrapper import MoleculeOneWrapper

m1wrapper/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
wrapper_version = 0.1
2+
api_token_version = 'v1'
3+
api_base_url = 'https://app.molecule.one/api/v1/'
4+
api_search_endpoint = 'batch-search'
5+
api_results_endpoint = 'batch-search-result'
6+
status_check_delay_s = 15

m1wrapper/m1wrapper.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import List, Dict
2+
3+
from .search import BatchSearch
4+
from .config import api_token_version, wrapper_version, api_base_url
5+
6+
7+
class MoleculeOneWrapper:
8+
"""
9+
Wrapper for MoleculeOne Batch Scoring REST API
10+
"""
11+
12+
def __init__(
13+
self,
14+
api_token: str,
15+
api_base_url: str = api_base_url
16+
):
17+
self.api_token = api_token
18+
self.api_base_url = f'{api_base_url}/' # ensure base_url ends with '/'
19+
self.request_headers = self.__prepare_request_headers()
20+
21+
def __prepare_request_headers(self) -> dict:
22+
return {
23+
'Content-Type': 'application/json',
24+
'User-Agent': f'api-wrapper-python/{wrapper_version}',
25+
'Authorization': f'ApiToken-{api_token_version} {self.api_token}'
26+
}
27+
28+
def run_batch_search(
29+
self,
30+
targets: List[str],
31+
parameters: Dict = None
32+
) -> BatchSearch:
33+
return BatchSearch(
34+
self.api_base_url,
35+
self.request_headers,
36+
targets=targets,
37+
parameters=parameters
38+
)
39+
40+
def get_batch_search(self, search_id: str) -> BatchSearch:
41+
return BatchSearch.from_id(
42+
self.api_base_url,
43+
self.request_headers,
44+
search_id
45+
)
46+
47+
def delete_batch_search(self, search_id: str):
48+
search = BatchSearch.from_id(
49+
self.api_base_url,
50+
self.request_headers,
51+
search_id
52+
)
53+
return search.delete()
54+

m1wrapper/search.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import requests
2+
import json
3+
import time
4+
from typing import List
5+
from urllib.parse import urljoin
6+
7+
from .config import (
8+
api_search_endpoint,
9+
api_results_endpoint,
10+
status_check_delay_s
11+
)
12+
13+
14+
def format_error_message(error):
15+
if error["message"] and error["errors"]:
16+
return f'{error["message"]}: {repr(error["errors"])}'
17+
if error["message"]:
18+
return f'{error["message"]}'
19+
else:
20+
return "unknown error"
21+
22+
23+
def maybe_handle_error(response):
24+
if response.status_code >= 400 and response.status_code <= 500:
25+
error = format_error_message(response.json())
26+
raise requests.exceptions.HTTPError(error)
27+
else:
28+
response.raise_for_status()
29+
30+
31+
class BatchSearch:
32+
def __init__(
33+
self,
34+
base_url,
35+
headers,
36+
search_id=None,
37+
targets=None,
38+
parameters=None,
39+
):
40+
self.search_id = search_id
41+
self.base_url = base_url
42+
self.headers = headers
43+
if self.search_id is None:
44+
new_search = self.__run(targets=targets, parameters=parameters)
45+
self.search_id = new_search['id']
46+
47+
def __prepare_payload(self, targets, parameters) -> dict:
48+
return {
49+
'targets': targets,
50+
'params': parameters or {},
51+
}
52+
53+
def __run(self, targets, parameters):
54+
payload = self.__prepare_payload(targets, parameters)
55+
url = urljoin(self.base_url, api_search_endpoint),
56+
response = requests.post(
57+
urljoin(self.base_url, api_search_endpoint),
58+
data=json.dumps(payload),
59+
headers=self.headers,
60+
)
61+
maybe_handle_error(response)
62+
return response.json()
63+
64+
@classmethod
65+
def from_id(cls, base_url, headers, search_id):
66+
return cls(base_url, headers, search_id)
67+
68+
def get_status(self):
69+
response = requests.get(
70+
urljoin(self.base_url, f'{api_search_endpoint}/{self.search_id}'),
71+
headers=self.headers,
72+
)
73+
maybe_handle_error(response)
74+
return response.json()
75+
76+
def is_finished(self):
77+
status = self.get_status()
78+
return status['queued'] == 0 and status['running'] == 0
79+
80+
def get_results(
81+
self,
82+
precision: int = None,
83+
only: List[str] = None
84+
):
85+
while self.is_finished() is False:
86+
time.sleep(status_check_delay_s)
87+
88+
return self.get_partial_results(precision, only)
89+
90+
def get_partial_results(
91+
self,
92+
precision: int = None,
93+
only: List[str] = None
94+
):
95+
response = requests.get(
96+
urljoin(self.base_url, f'{api_results_endpoint}/{self.search_id}'),
97+
headers=self.headers,
98+
params={
99+
'precision': precision,
100+
'only': only
101+
}
102+
)
103+
maybe_handle_error(response)
104+
return response.json()
105+
106+
def delete(self):
107+
response = requests.delete(
108+
urljoin(self.base_url, f'{api_search_endpoint}/{self.search_id}'),
109+
headers=self.headers,
110+
)
111+
maybe_handle_error(response)
112+
return True

requirements.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)