Skip to content

Commit 73b3fc9

Browse files
authored
Merge pull request #43 from 3D-e-Chem/json-error-42
Json error 42
2 parents c3ec658 + 9e71a1a commit 73b3fc9

19 files changed

+1022
-577
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@ Formatted as described on http://keepachangelog.com/.
55

66
## Unreleased
77

8-
### Fixes
8+
## [2.2.0] - 2017-02-23
9+
10+
### Changed
11+
12+
- Canned methods can now raise exception with ids which could not be found and data for ids which could
13+
14+
### Fixed
915

1016
- Fetch fragment with no molblock throws error (#41)
17+
- Not found response of web service should be JSON (#42)
1118

1219
## [2.1.0] - 2017-01-17
1320

appveyor.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ install:
1414
build_script:
1515
- cmd: pip install -r requirements.txt
1616
test_script:
17-
- cmd: pytest --ignore .\tests\test_frozen.py --ignore .\tests\test_script_dive.py --junitxml=junit-results.xml
17+
# TODO dont exclude files
18+
- cmd: pytest --ignore .\tests\test_frozen.py --ignore .\tests\script\test_dive.py --junitxml=junit-results.xml
1819
on_finish:
1920
- ps: >-
2021
$url = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)"

kripodb/canned.py

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,27 @@
1818

1919
from __future__ import absolute_import
2020

21+
import numpy as np
2122
import pandas as pd
23+
from requests import HTTPError
2224

2325
from .db import FragmentsDb
2426
from .pairs import similar, open_similarity_matrix
25-
from .webservice.client import WebserviceClient
27+
from .webservice.client import WebserviceClient, IncompleteFragments
28+
29+
30+
class IncompleteHits(Exception):
31+
def __init__(self, absent_identifiers, hits):
32+
"""List of hits and list of identifiers for which no information could be found
33+
34+
Args:
35+
absent_identifiers (List[str]): List of identifiers for which no information could be found
36+
hits (pandas.DataFrame): Data frame with query_fragment_id, hit_frag_id and score columns
37+
"""
38+
message = 'Some query fragment identifiers could not be found'
39+
super(IncompleteHits, self).__init__(message)
40+
self.absent_identifiers = absent_identifiers
41+
self.hits = hits
2642

2743

2844
def similarities(queries, similarity_matrix_filename_or_url, cutoff, limit=1000):
@@ -54,26 +70,48 @@ def similarities(queries, similarity_matrix_filename_or_url, cutoff, limit=1000)
5470
5571
Returns:
5672
pandas.DataFrame: Data frame with query_fragment_id, hit_frag_id and score columns
73+
74+
Raises:
75+
IncompleteHits: When one or more of the identifiers could not be found.
5776
"""
5877
hits = []
78+
absent_identifiers = []
5979
if similarity_matrix_filename_or_url.startswith('http'):
6080
client = WebserviceClient(similarity_matrix_filename_or_url)
6181
for query in queries:
62-
qhits = client.similar_fragments(query, cutoff, limit)
63-
hits.extend(qhits)
82+
try:
83+
qhits = client.similar_fragments(query, cutoff, limit)
84+
hits.extend(qhits)
85+
except HTTPError as e:
86+
if e.response.status_code == 404:
87+
absent_identifiers.append(query)
6488
else:
6589
similarity_matrix = open_similarity_matrix(similarity_matrix_filename_or_url)
6690
for query in queries:
67-
for query_id, hit_id, score in similar(query, similarity_matrix, cutoff, limit):
68-
hit = {'query_frag_id': query_id,
69-
'hit_frag_id': hit_id,
70-
'score': score,
71-
}
72-
hits.append(hit)
91+
try:
92+
for query_id, hit_id, score in similar(query, similarity_matrix, cutoff, limit):
93+
hit = {'query_frag_id': query_id,
94+
'hit_frag_id': hit_id,
95+
'score': score,
96+
}
97+
hits.append(hit)
98+
except KeyError:
99+
absent_identifiers.append(query)
73100

74101
similarity_matrix.close()
75102

76-
return pd.DataFrame(hits)
103+
if absent_identifiers:
104+
if len(hits) > 0:
105+
df = pd.DataFrame(hits, columns=['query_frag_id', 'hit_frag_id', 'score'])
106+
else:
107+
# empty hits array will give dataframe without columns
108+
df = pd.DataFrame({'query_frag_id': pd.Series(dtype=str),
109+
'hit_frag_id': pd.Series(dtype=str),
110+
'score': pd.Series(dtype=np.double)
111+
}, columns=['query_frag_id', 'hit_frag_id', 'score'])
112+
raise IncompleteHits(absent_identifiers, df)
113+
114+
return pd.DataFrame(hits, columns=['query_frag_id', 'hit_frag_id', 'score'])
77115

78116

79117
def fragments_by_pdb_codes(pdb_codes, fragments_db_filename_or_url, prefix=''):
@@ -104,16 +142,32 @@ def fragments_by_pdb_codes(pdb_codes, fragments_db_filename_or_url, prefix=''):
104142
105143
Returns:
106144
pandas.DataFrame: Data frame with fragment information
145+
146+
Raises:
147+
IncompleteFragments: When one or more of the identifiers could not be found.
107148
"""
108149
if fragments_db_filename_or_url.startswith('http'):
109150
client = WebserviceClient(fragments_db_filename_or_url)
110-
fragments = client.fragments_by_pdb_codes(pdb_codes)
151+
try:
152+
fragments = client.fragments_by_pdb_codes(pdb_codes)
153+
except IncompleteFragments as e:
154+
df = pd.DataFrame(e.fragments)
155+
df.rename(columns=lambda x: prefix + x, inplace=True)
156+
raise IncompleteFragments(e.absent_identifiers, df)
111157
else:
112158
fragmentsdb = FragmentsDb(fragments_db_filename_or_url)
113159
fragments = []
160+
absent_identifiers = []
114161
for pdb_code in pdb_codes:
115-
for fragment in fragmentsdb.by_pdb_code(pdb_code):
116-
fragments.append(fragment)
162+
try:
163+
for fragment in fragmentsdb.by_pdb_code(pdb_code):
164+
fragments.append(fragment)
165+
except LookupError as e:
166+
absent_identifiers.append(pdb_code)
167+
if absent_identifiers:
168+
df = pd.DataFrame(fragments)
169+
df.rename(columns=lambda x: prefix + x, inplace=True)
170+
raise IncompleteFragments(absent_identifiers, df)
117171

118172
df = pd.DataFrame(fragments)
119173
df.rename(columns=lambda x: prefix + x, inplace=True)
@@ -146,13 +200,31 @@ def fragments_by_id(fragment_ids, fragments_db_filename_or_url, prefix=''):
146200
147201
Returns:
148202
pandas.DataFrame: Data frame with fragment information
203+
204+
Raises:
205+
IncompleteFragments: When one or more of the identifiers could not be found.
149206
"""
150207
if fragments_db_filename_or_url.startswith('http'):
151208
client = WebserviceClient(fragments_db_filename_or_url)
152-
fragments = client.fragments_by_id(fragment_ids)
209+
try:
210+
fragments = client.fragments_by_id(fragment_ids)
211+
except IncompleteFragments as e:
212+
df = pd.DataFrame(e.fragments)
213+
df.rename(columns=lambda x: prefix + x, inplace=True)
214+
raise IncompleteFragments(e.absent_identifiers, df)
153215
else:
154216
fragmentsdb = FragmentsDb(fragments_db_filename_or_url)
155-
fragments = [fragmentsdb[frag_id] for frag_id in fragment_ids]
217+
fragments = []
218+
absent_identifiers = []
219+
for frag_id in fragment_ids:
220+
try:
221+
fragments.append(fragmentsdb[frag_id])
222+
except KeyError:
223+
absent_identifiers.append(frag_id)
224+
if absent_identifiers:
225+
df = pd.DataFrame(fragments)
226+
df.rename(columns=lambda x: prefix + x, inplace=True)
227+
raise IncompleteFragments(absent_identifiers, df)
156228

157229
df = pd.DataFrame(fragments)
158230
df.rename(columns=lambda x: prefix + x, inplace=True)

kripodb/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
__version_info__ = ('2', '1', '0')
15+
__version_info__ = ('2', '2', '0')
1616
__version__ = '.'.join(__version_info__)

kripodb/webservice/client.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@
1515
from __future__ import absolute_import
1616
import requests
1717
from rdkit.Chem.AllChem import MolFromMolBlock
18+
from requests import HTTPError
19+
20+
21+
class IncompleteFragments(Exception):
22+
23+
def __init__(self, absent_identifiers, fragments):
24+
"""List of fragments and list of identifiers for which no information could be found
25+
26+
Args:
27+
absent_identifiers (List[str]): List of identifiers for which no information could be found
28+
fragments (List[dict]): List of fragment information that could be retrieved
29+
"""
30+
message = 'Some identifiers could not be found'
31+
super(IncompleteFragments, self).__init__(message)
32+
self.absent_identifiers = absent_identifiers
33+
self.fragments = fragments
1834

1935

2036
class WebserviceClient(object):
@@ -41,6 +57,9 @@ def similar_fragments(self, fragment_id, cutoff, limit=1000):
4157
4258
Returns:
4359
list[dict]: Query fragment identifier, hit fragment identifier and similarity score
60+
61+
Raises:
62+
request.HTTPError: When fragment_id could not be found
4463
"""
4564
url = self.base_url + '/fragments/{fragment_id}/similar'.format(fragment_id=fragment_id)
4665
params = {'cutoff': cutoff, 'limit': limit}
@@ -74,24 +93,36 @@ def fragments_by_id(self, fragment_ids, chunk_size=100):
7493
list[dict]: List of fragment information
7594
7695
Raises:
77-
requests.HTTPError: When one of the identifiers could not be found.
96+
IncompleteFragments: When one or more of the identifiers could not be found.
7897
"""
7998
return self._fetch_chunked_fragments('fragment_ids', fragment_ids, chunk_size)
8099

81100
def _fetch_chunked_fragments(self, idtype, ids, chunk_size):
82101
fragments = []
102+
absent_identifiers = []
83103
for start in range(0, len(ids), chunk_size):
84104
stop = chunk_size + start
85-
fragments += self._fetch_fragments(idtype, ids[start:stop])
105+
(chunk_fragments, chunk_absent_identifiers) = self._fetch_fragments(idtype, ids[start:stop])
106+
fragments += chunk_fragments
107+
absent_identifiers += chunk_absent_identifiers
108+
if chunk_absent_identifiers:
109+
raise IncompleteFragments(absent_identifiers, fragments)
86110
return fragments
87111

88112
def _fetch_fragments(self, idtype, ids):
89113
url = self.base_url + '/fragments?{idtype}={ids}'.format(idtype=idtype, ids=','.join(ids))
90-
response = requests.get(url)
91-
response.raise_for_status()
92-
fragments = response.json()
114+
absent_identifiers = []
115+
try:
116+
response = requests.get(url)
117+
response.raise_for_status()
118+
fragments = response.json()
119+
except HTTPError as e:
120+
if e.response.status_code == 404:
121+
body = e.response.json()
122+
fragments = body['fragments']
123+
absent_identifiers = body['absent_identifiers']
93124
# Convert molblock string to RDKit Mol object
94125
for fragment in fragments:
95126
if fragment['mol'] is not None:
96127
fragment['mol'] = MolFromMolBlock(fragment['mol'])
97-
return fragments
128+
return fragments, absent_identifiers

kripodb/webservice/server.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"""Kripo datafiles wrapped in a webservice"""
1515
from __future__ import absolute_import
1616

17-
from kripodb.db import FragmentsDb
1817
from pkg_resources import resource_filename
1918
import logging
2019

@@ -24,11 +23,12 @@
2423
from six.moves.urllib_parse import urlparse
2524

2625
import connexion
27-
from flask import current_app, abort
26+
from flask import current_app
2827
from flask.json import JSONEncoder
2928

30-
from ..version import __version__
29+
from ..db import FragmentsDb
3130
from ..pairs import open_similarity_matrix
31+
from ..version import __version__
3232

3333
LOGGER = logging.getLogger(__name__)
3434

@@ -73,10 +73,17 @@ def get_similar_fragments(fragment_id, cutoff, limit):
7373
for hit_id, score in raw_hits:
7474
hits.append({'query_frag_id': query_id, 'hit_frag_id': hit_id, 'score': score})
7575
except LookupError:
76-
abort(404, 'Fragment with identifier \'{0}\' not found'.format(fragment_id))
76+
return fragment_not_found(fragment_id)
7777
return hits
7878

7979

80+
def fragment_not_found(fragment_id):
81+
title = 'Not Found'
82+
description = 'Fragment with identifier \'{0}\' not found'.format(fragment_id)
83+
ext = {'identifier': fragment_id}
84+
return connexion.problem(404, title, description, ext=ext)
85+
86+
8087
def get_fragments(fragment_ids=None, pdb_codes=None):
8188
"""Retrieve fragments based on their identifier or PDB code.
8289
@@ -93,20 +100,34 @@ def get_fragments(fragment_ids=None, pdb_codes=None):
93100
fragments_db_filename = current_app.config['db_fn']
94101
with FragmentsDb(fragments_db_filename) as fragmentsdb:
95102
fragments = []
103+
missing_ids = []
96104
if fragment_ids:
97105
for frag_id in fragment_ids:
98106
try:
99107
fragments.append(fragmentsdb[frag_id])
100108
except LookupError:
101-
abort(404, 'Fragment with identifier \'{0}\' not found'.format(frag_id))
109+
missing_ids.append(frag_id)
110+
102111
if pdb_codes:
103112
for pdb_code in pdb_codes:
104113
try:
105114
for fragment in fragmentsdb.by_pdb_code(pdb_code):
106115
fragments.append(fragment)
107116
except LookupError:
108-
abort(404, 'Fragments with PDB code \'{0}\' not found'.format(pdb_code))
117+
missing_ids.append(pdb_code)
109118
# TODO if fragment_ids and pdb_codes are both None then return paged list of all fragments
119+
if missing_ids:
120+
title = 'Not found'
121+
label = 'identifiers'
122+
if pdb_codes:
123+
label = 'PDB codes'
124+
description = 'Fragments with {1} \'{0}\' not found'.format(','.join(missing_ids), label)
125+
# connexion.problem is using json.dumps instead of flask custom json encoder, so performing convert myself
126+
# TODO remove mol2string conversion when https://github.com/zalando/connexion/issues/266 is fixed
127+
for fragment in fragments:
128+
fragment['mol'] = MolToMolBlock(fragment['mol'])
129+
ext = {'absent_identifiers': missing_ids, 'fragments': fragments}
130+
return connexion.problem(404, title, description, ext=ext)
110131
return fragments
111132

112133

@@ -133,11 +154,10 @@ def get_fragment_svg(fragment_id, width, height):
133154
with FragmentsDb(fragments_db_filename) as fragmentsdb:
134155
try:
135156
fragment = fragmentsdb[fragment_id]
136-
LOGGER.warning([fragment_id, width, height])
137157
mol = fragment['mol']
138158
return mol2svg(mol, width, height)
139159
except LookupError:
140-
abort(404, 'Fragment with identifier \'{0}\' not found'.format(fragment_id))
160+
return fragment_not_found(fragment_id)
141161

142162

143163
def get_version():
@@ -162,11 +182,14 @@ def wsgi_app(sim_matrix, frags_db_fn, external_url='http://localhost:8084/kripo'
162182
"""
163183
app = connexion.App(__name__)
164184
url = urlparse(external_url)
165-
swagger_file = resource_filename(__name__, 'swagger.json')
166-
app.add_api(swagger_file, base_path=url.path, arguments={'hostport': url.netloc, 'scheme': url.scheme})
185+
swagger_file = resource_filename(__name__, 'swagger.yaml')
167186
app.app.json_encoder = KripodbJSONEncoder
168187
app.app.config['matrix'] = sim_matrix
169188
app.app.config['db_fn'] = frags_db_fn
189+
arguments = {'hostport': url.netloc, 'scheme': url.scheme, 'version': __version__}
190+
# Keep validate_responses turned off, because of conflict with connexion.problem
191+
# see https://github.com/zalando/connexion/issues/266
192+
app.add_api(swagger_file, base_path=url.path, arguments=arguments)
170193
return app
171194

172195

0 commit comments

Comments
 (0)