Skip to content

Commit 3777a48

Browse files
authored
Merge branch 'main' into add-metakg-endpoint
2 parents 4bcd99b + c008226 commit 3777a48

File tree

12 files changed

+225
-107
lines changed

12 files changed

+225
-107
lines changed

src/handlers/api.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from controller.exceptions import ControllerError, NotFoundError
1818
from pipeline import MetaKGQueryPipeline
1919
from utils.downloader import DownloadError, download_async
20+
from utils.http_error import SmartAPIHTTPError
2021
from utils.metakg.biolink_helpers import get_expanded_values
2122
from utils.metakg.cytoscape_formatter import CytoscapeDataFormatter
2223
from utils.metakg.export import edges2graphml
@@ -68,7 +69,7 @@ def get(self):
6869
# raising HTTPError will cause headers to be emptied
6970
self.finish()
7071
else:
71-
raise HTTPError(403)
72+
raise HTTPError(status_code=403)
7273

7374

7475
class LoginHandler(AuthHandler):
@@ -113,7 +114,7 @@ class ValidateHandler(BaseHandler):
113114

114115
async def get(self):
115116
if self.request.body:
116-
raise HTTPError(400, reason="GET takes no request body.")
117+
raise HTTPError(status_code=400, reason="GET takes no request body.")
117118

118119
raw = await self.download(self.args.url)
119120
self.validate(raw)
@@ -139,9 +140,8 @@ def validate(self, raw):
139140
smartapi = SmartAPI(SmartAPI.VALIDATION_ONLY)
140141
smartapi.raw = raw
141142
smartapi.validate()
142-
143143
except (ControllerError, AssertionError) as err:
144-
raise HTTPError(400, reason=str(err))
144+
raise SmartAPIHTTPError(400, reason=str(err))
145145
else:
146146
self.finish({"success": True, "details": f"valid SmartAPI ({smartapi.version}) metadata."})
147147

@@ -165,19 +165,19 @@ async def post(self):
165165
"""
166166

167167
if SmartAPI.find(self.args.url, "url"):
168-
raise HTTPError(409)
168+
raise HTTPError(status_code=409)
169169

170170
try:
171171
file = await download_async(self.args.url)
172172
except DownloadError as err:
173-
raise HTTPError(400, reason=str(err)) from err
173+
raise HTTPError(status_code=400, reason=str(err)) from err
174174

175175
try:
176176
smartapi = SmartAPI(self.args.url)
177177
smartapi.raw = file.raw
178178
smartapi.validate()
179179
except (ControllerError, AssertionError) as err:
180-
raise HTTPError(400, reason=str(err)) from err
180+
raise HTTPError(status_code=400, reason=str(err)) from err
181181

182182
if self.args.dryrun:
183183
raise Finish({"success": True, "details": f"[Dryrun] Valid {smartapi.version} Metadata"})
@@ -187,7 +187,7 @@ async def post(self):
187187
smartapi.refresh(file) # populate webdoc meta
188188
_id = smartapi.save()
189189
except ControllerError as err:
190-
raise HTTPError(400, reason=str(err)) from err
190+
raise HTTPError(status_code=400, reason=str(err)) from err
191191
else:
192192
self.finish({"success": True, "_id": _id})
193193
await self._notify(smartapi)
@@ -252,21 +252,21 @@ async def put(self, _id):
252252
try:
253253
smartapi = SmartAPI.get(_id)
254254
except NotFoundError:
255-
raise HTTPError(404)
255+
raise HTTPError(status_code=404)
256256

257257
if smartapi.username != self.current_user["login"]:
258-
raise HTTPError(403)
258+
raise HTTPError(status_code=403)
259259

260260
if self.args.slug is not None:
261261
if self.args.slug in {"api"}: # reserved
262-
raise HTTPError(400, reason="slug is reserved")
262+
raise HTTPError(status_code=400, reason="slug is reserved")
263263

264264
try: # update slug
265265
smartapi.slug = self.args.slug or None
266266
smartapi.save()
267267

268268
except (ControllerError, ValueError) as err:
269-
raise HTTPError(400, reason=str(err)) from err
269+
raise HTTPError(status_code=400, reason=str(err)) from err
270270

271271
self.finish({"success": True})
272272

@@ -292,15 +292,15 @@ def delete(self, _id):
292292
try:
293293
smartapi = SmartAPI.get(_id)
294294
except NotFoundError:
295-
raise HTTPError(404)
295+
raise HTTPError(status_code=404)
296296

297297
if smartapi.username != self.current_user["login"]:
298-
raise HTTPError(403)
298+
raise HTTPError(status_code=403)
299299

300300
try:
301301
_id = smartapi.delete()
302302
except ControllerError as err:
303-
raise HTTPError(400, reason=str(err)) from err
303+
raise HTTPError(status_code=400, reason=str(err)) from err
304304

305305
self.finish({"success": True, "_id": _id})
306306

@@ -346,41 +346,41 @@ class UptimeHandler(BaseHandler):
346346
@github_authenticated
347347
def get(self):
348348
if self.request.body:
349-
raise HTTPError(400, reason="GET takes no request body.")
349+
raise HTTPError(status_code=400, reason="GET takes no request body.")
350350

351351
if self.args.id:
352352
try:
353353
smartapi = SmartAPI.get(self.args.id)
354354
if smartapi.username != self.current_user["login"]:
355-
raise HTTPError(403)
355+
raise HTTPError(status_code=403)
356356
status = smartapi.check()
357357
smartapi.save()
358358
except NotFoundError:
359-
raise HTTPError(404)
359+
raise HTTPError(status_code=404)
360360
except (ControllerError, AssertionError) as err:
361-
raise HTTPError(400, reason=str(err))
361+
raise HTTPError(status_code=400, reason=str(err))
362362
else:
363363
self.finish({"success": True, "details": status})
364364
else:
365-
raise HTTPError(400, reason="Missing required parameter: id")
365+
raise HTTPError(status_code=400, reason="Missing required parameter: id")
366366

367367
@github_authenticated
368368
def post(self):
369369
if self.args.id:
370370
try:
371371
smartapi = SmartAPI.get(self.args.id)
372372
if smartapi.username != self.current_user["login"]:
373-
raise HTTPError(403)
373+
raise HTTPError(status_code=403)
374374
status = smartapi.check()
375375
smartapi.save()
376376
except NotFoundError:
377-
raise HTTPError(404)
377+
raise HTTPError(status_code=404)
378378
except (ControllerError, AssertionError) as err:
379-
raise HTTPError(400, reason=str(err))
379+
raise HTTPError(status_code=400, reason=str(err))
380380
else:
381381
self.finish({"success": True, "details": status})
382382
else:
383-
raise HTTPError(400, reason="Missing required form field: id")
383+
raise HTTPError(status_code=400, reason="Missing required form field: id")
384384

385385

386386
class MetaKGHandlerMixin:
@@ -600,14 +600,14 @@ def write(self, chunk):
600600

601601
class MetaKGPathFinderHandler(QueryHandler):
602602
"""
603-
A handler for querying paths in a knowledge graph using MetaKGPathFinder.
603+
A handler for querying paths in a knowledge graph using the custom MetaKGPathFinder module.
604604
605605
Attributes:
606606
- name: Unique identifier for this handler.
607607
- kwargs: Configuration for GET request parameters.
608608
609-
The primary GET method accepts 'subject', 'object', and 'cutoff' parameters, then retrieves
610-
and returns paths in JSON format between the specified entities up to the given 'cutoff' length.
609+
The primary GET method accepts the required 'subject', 'object', and 'cutoff'(default=3) parameters, then retrieves
610+
and returns paths in JSON format between the specified nodes up to the given 'cutoff' length.
611611
"""
612612

613613
name = "metakgpathfinder"
@@ -672,6 +672,11 @@ def setup_pathfinder_rawquery(self, expanded_fields):
672672

673673
@capture_exceptions
674674
async def get(self, *args, **kwargs):
675+
676+
# Check if subject and object are the same - not allowed
677+
if self.args.subject == self.args.object:
678+
raise ValueError("Subject and object must be different.")
679+
675680
query_data = {"q": self.args.q}
676681

677682
# Initialize with the original subject and object, and setup for expansion
@@ -715,6 +720,10 @@ async def get(self, *args, **kwargs):
715720
bte=self.args.bte
716721
)
717722

723+
# # Error check path results
724+
if "error" in paths_with_edges:
725+
raise HTTPError(400, reason=str(paths_with_edges["error"]))
726+
718727
# Check if rawquery parameter is true -- respond with correct output
719728
if self.args.rawquery:
720729
raw_query_output = self.setup_pathfinder_rawquery(expanded_fields)

src/index.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" SmartAPI Entry Point """
22

33
import logging
4+
from os.path import exists
45

56
from threading import Thread
67

@@ -20,7 +21,8 @@ def run_routine():
2021

2122
class WebAppHandler(RequestHandler):
2223
def get(self):
23-
self.render("../web-app/dist/index.html")
24+
if exists("../web-app/dist/index.html"):
25+
self.render("../web-app/dist/index.html")
2426

2527

2628
if __name__ == "__main__":

src/utils/downloader.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ def get_raw(self):
121121

122122
def download(url, timeout=5, raise_error=True):
123123
try:
124-
response = requests.get(url, timeout=timeout)
124+
headers = {
125+
'User-Agent': 'SmartAPI' # UA required by GitHub
126+
}
127+
response = requests.get(url, headers=headers, timeout=timeout)
125128
if raise_error:
126129
response.raise_for_status()
127130
result = RequestsParser(response)
@@ -151,7 +154,10 @@ async def download_async(url, timeout=20, raise_error=True):
151154

152155

153156
def download_mapping(url):
154-
response = requests.get(url)
157+
headers = {
158+
'User-Agent': 'SmartAPI' # UA required by GitHub
159+
}
160+
response = requests.get(url, headers=headers, timeout=60)
155161
response.raise_for_status()
156162

157163
return decoder.to_dict(stream=response.content, ext=file_extension(url), ctype=response.headers.get("Content-Type"))

src/utils/http_error.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Optional, Any
2+
from tornado.web import HTTPError
3+
import re
4+
5+
6+
class SmartAPIHTTPError(HTTPError):
7+
"""An extended HTTPError class with additional details and message sanitization.
8+
9+
Adds the following enhancements:
10+
- A `details` parameter for including extra context about the error.
11+
- A `clean_error_message` method for sanitizing log messages and details.
12+
13+
:arg str details: Additional information about the error.
14+
"""
15+
16+
def __init__(
17+
self,
18+
status_code: int = 500,
19+
log_message: Optional[str] = None,
20+
*args: Any,
21+
**kwargs: Any,
22+
) -> None:
23+
super().__init__(status_code, log_message, *args, **kwargs)
24+
if self.reason:
25+
self.reason = self.clean_error_message(self.reason)
26+
if self.log_message:
27+
self.log_message = self.clean_error_message(self.log_message)
28+
29+
@staticmethod
30+
def clean_error_message(message: str) -> str:
31+
"""
32+
Sanitizes an error message by replacing newlines, tabs, and reducing multiple spaces.
33+
34+
:param message: The error message to sanitize.
35+
:return: A cleaned and sanitized version of the message.
36+
"""
37+
message = message.replace("\n", " ") # Replace actual newlines with spaces
38+
message = re.sub(r'\s+', ' ', message) # Normalize spaces
39+
return message.strip()

src/utils/metakg/path_finder.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import logging
2-
31
import networkx as nx
2+
43
from controller.metakg import MetaKG
54
from model import ConsolidatedMetaKGDoc
65

7-
logger = logging.basicConfig(level=logging.INFO, filename="missing_bte.log")
8-
9-
106
class MetaKGPathFinder:
117
def __init__(self, query_data=None, expanded_fields=None):
128
"""
@@ -104,43 +100,51 @@ def get_paths(self, cutoff=2, api_details=False, predicate_filter=None, bte=Fals
104100
If True, includes full details of the 'api' in the result.
105101
- predicate_filter: list (default=None)
106102
A list of predicates to filter the results by.
107-
103+
- bte: bool (default=False)
104+
If True, includes BTE information in the result.
108105
Returns:
109106
- all_paths_with_edges: list of dict
110107
A list containing paths and their edge information for all subject-object pairs.
111108
"""
112109

113110
all_paths_with_edges = []
114111

115-
# Predicate Filter Setup
116112
predicate_filter_set = set(predicate_filter) if predicate_filter else None
113+
117114
if 'predicate' in self.expanded_fields and self.expanded_fields['predicate']:
118115
predicate_filter_set.update(self.expanded_fields['predicate'])
119116

120-
# Graph iteration over subject-object pairs
121-
for subject in self.expanded_fields["subject"]:
122-
for object in self.expanded_fields["object"]:
123-
try:
124-
# Check if a path exists between the subject and object
125-
if nx.has_path(self.G, subject, object):
126-
raw_paths = nx.all_simple_paths(self.G, source=subject, target=object, cutoff=cutoff)
127-
for path in raw_paths:
128-
paths_data = {"path": path, "edges": []}
129-
edge_added = False
130-
for i in range(len(path) - 1):
131-
source_node = path[i]
132-
target_node = path[i + 1]
133-
edge_key = f"{source_node}-{target_node}"
134-
edge_data = self.predicates.get(edge_key, [])
135-
136-
for data in edge_data:
137-
if predicate_filter_set and data["predicate"] not in predicate_filter_set:
138-
continue
139-
paths_data = self.build_edge_results(paths_data, data, api_details, source_node, target_node, bte)
140-
edge_added = True
141-
if edge_added:
142-
all_paths_with_edges.append(paths_data)
143-
except Exception:
144-
continue
145-
146-
return all_paths_with_edges
117+
try:
118+
# Graph iteration over subject-object pairs
119+
for subject in self.expanded_fields["subject"]:
120+
for object in self.expanded_fields["object"]:
121+
if subject not in self.G:
122+
return { "error": f"Subject node {subject} is not found in the MetaKG" }
123+
if object not in self.G:
124+
return { "error": f"Object node {object} is not found in the MetaKG" }
125+
try:
126+
# Check if a path exists between the subject and object
127+
if nx.has_path(self.G, subject, object):
128+
raw_paths = nx.all_simple_paths(self.G, source=subject, target=object, cutoff=cutoff)
129+
for path in raw_paths:
130+
paths_data = {"path": path, "edges": []}
131+
edge_added = False
132+
for i in range(len(path) - 1):
133+
source_node = path[i]
134+
target_node = path[i + 1]
135+
edge_key = f"{source_node}-{target_node}"
136+
edge_data = self.predicates.get(edge_key, [])
137+
138+
for data in edge_data:
139+
if predicate_filter_set and data["predicate"] not in predicate_filter_set:
140+
continue
141+
paths_data = self.build_edge_results(paths_data, data, api_details, source_node, target_node, bte)
142+
edge_added = True
143+
if edge_added:
144+
all_paths_with_edges.append(paths_data)
145+
except nx.exception.NodeNotFound as node_err:
146+
return { "error": node_err }
147+
return all_paths_with_edges
148+
149+
except Exception as e:
150+
return { "error": e }

src/utils/monitor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,10 @@ def make_api_call(self):
295295
if "example" in _param:
296296
# parameter in path
297297
if _param["in"] == "path":
298-
url = url.replace("{" + _param["name"] + "}", _param["example"])
298+
url = str(url).replace("{" + _param["name"] + "}", str(_param["example"]))
299299
# parameter in query
300300
elif _param["in"] == "query":
301-
params = {_param["name"]: _param["example"]}
301+
params[_param["name"]] = _param["example"]
302302

303303
elif "required" in _param and _param["required"] is True:
304304
paramsRequired = True # pylint: disable=invalid-name

0 commit comments

Comments
 (0)