-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backend for PKG visualization #76
Changes from all commits
037f3d6
5d82796
876d650
e0ac3cc
c6ae304
b5d9507
f91a565
df51de5
946d2e7
ec7b53a
daff0d5
a350ae4
9f8c163
6ad2043
c4fd937
438c8c3
7ff6d05
fd3cf4f
fb79bd6
931e4a0
3ead65f
9ab4cbe
a50e064
1ecb272
858150d
54ec73a
84bd4a4
2cf8ccb
4cef2ab
1d124ab
f27bdb8
5777920
7f82a76
f17f23e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# PKG Visualizations |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,26 +8,43 @@ | |
can be found here: https://github.com/iai-group/pkg-vocabulary | ||
""" | ||
|
||
import io | ||
from typing import Dict, Optional | ||
|
||
import pydotplus | ||
from IPython.display import display | ||
from rdflib.query import Result | ||
from rdflib.term import Variable | ||
from rdflib.tools.rdf2dot import rdf2dot | ||
|
||
import pkg_api.utils as utils | ||
from pkg_api.connector import Connector, RDFStore | ||
from pkg_api.core.namespaces import PKGPrefixes | ||
from pkg_api.core.pkg_types import URI | ||
|
||
DEFAULT_VISUALIZATION_PATH = "data/pkg_visualizations" | ||
|
||
|
||
class PKG: | ||
def __init__(self, owner: URI, rdf_store: RDFStore, rdf_path: str) -> None: | ||
def __init__( | ||
self, | ||
owner: URI, | ||
rdf_store: RDFStore, | ||
rdf_path: str, | ||
visualization_path: str = DEFAULT_VISUALIZATION_PATH, | ||
) -> None: | ||
"""Initializes PKG of a given user. | ||
|
||
Args: | ||
owner: Owner URI. | ||
rdf_store: Type of RDF store. | ||
rdf_path: Path to the RDF store. | ||
visualization_path: Path to the visualization of PKG. Defaults to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong indentation (extra spaces) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indentations follows our guidelines |
||
DEFAULT_VISUALIZATION_PATH. | ||
""" | ||
self._owner_uri = owner | ||
self._connector = Connector(owner, rdf_store, rdf_path) | ||
self._visualization_path = visualization_path | ||
|
||
@property | ||
def owner_uri(self) -> URI: | ||
|
@@ -116,3 +133,40 @@ def add_statement(self, pkg_data: utils.PKGData) -> None: | |
""" | ||
query = utils.get_query_for_add_statement(pkg_data) | ||
self._connector.execute_sparql_update(query) | ||
|
||
def execute_sparql_query(self, query: str) -> Result: | ||
"""Executes a SPARQL query. | ||
|
||
Args: | ||
query: SPARQL query. | ||
|
||
Returns: | ||
Result of the SPARQL query. | ||
""" | ||
WerLaj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return self._connector.execute_sparql_query(query) | ||
|
||
def visualize_graph(self) -> str: | ||
"""Visualizes the PKG. | ||
|
||
https://stackoverflow.com/questions/39274216/visualize-an-rdflib-graph-in-python # noqa: E501 | ||
|
||
Returns: | ||
The path to the image visualizing the PKG. | ||
""" | ||
stream = io.StringIO() | ||
rdf2dot(self._connector._graph, stream, opts={display}) | ||
dg = pydotplus.graph_from_dot_data(stream.getvalue()) | ||
png = dg.create_png() | ||
|
||
owner_name = "" | ||
|
||
for _, namespace in PKGPrefixes.__members__.items(): | ||
if namespace.value in str(self._owner_uri): | ||
owner_name = self._owner_uri.replace(str(namespace.value), "") | ||
|
||
path = self._visualization_path + "/" + owner_name + ".png" | ||
|
||
with open(path, "wb") as test_png: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this have error handling? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All images are for now saved into the same, predefined directory that is created locally in the repo. So it should be fine. |
||
test_png.write(png) | ||
|
||
return path |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,61 @@ | ||
"""PKG Exploration Resource.""" | ||
from typing import Dict | ||
from typing import Any, Dict, Tuple | ||
|
||
from flask import request | ||
from flask_restful import Resource | ||
|
||
from pkg_api.server.utils import open_pkg, parse_query_request_data | ||
|
||
|
||
class PKGExplorationResource(Resource): | ||
def get(self) -> Dict[str, str]: | ||
"""Returns the data for PKG exploration.""" | ||
return {"message": "PKG Exploration"} | ||
def get(self) -> Tuple[Dict[str, Any], int]: | ||
"""Returns the PKG visualization. | ||
|
||
Returns: | ||
A dictionary with the path to PKG visualization and the status code. | ||
""" | ||
data = request.json | ||
try: | ||
pkg = open_pkg(data) | ||
except Exception as e: | ||
return {"message": str(e)}, 400 | ||
|
||
graph_img_path = pkg.visualize_graph() | ||
pkg.close() | ||
|
||
return { | ||
"message": "PKG visualized successfully.", | ||
"img_path": graph_img_path, | ||
}, 200 | ||
|
||
def post(self) -> Tuple[Dict[str, Any], int]: | ||
"""Executes the SPARQL query. | ||
|
||
Returns: | ||
A dictionary with the result of running SPARQL query and the status | ||
code. | ||
""" | ||
data = request.json | ||
try: | ||
pkg = open_pkg(data) | ||
except Exception as e: | ||
return {"message": str(e)}, 400 | ||
|
||
sparql_query = parse_query_request_data(data) | ||
|
||
if "SELECT" in sparql_query: | ||
result = str(pkg.execute_sparql_query(sparql_query)) | ||
else: | ||
return { | ||
"message": ( | ||
"Operation is not supported. Provide SPARQL select " | ||
"query." | ||
) | ||
}, 400 | ||
|
||
pkg.close() | ||
|
||
return { | ||
"message": "SPARQL query executed successfully.", | ||
"data": result, | ||
}, 200 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
"""Utility functions for the server.""" | ||
from typing import Any, Dict | ||
|
||
from flask import current_app | ||
|
||
from pkg_api.connector import RDFStore | ||
from pkg_api.core.pkg_types import URI | ||
from pkg_api.pkg import PKG | ||
|
||
|
||
def open_pkg(data: Dict[str, str]) -> PKG: | ||
"""Opens a connection to the PKG. | ||
|
||
Args: | ||
data: Request data. | ||
|
||
Returns: | ||
A PKG instance. | ||
""" | ||
owner_uri = data.get("owner_uri", None) | ||
owner_username = data.get("owner_username", None) | ||
if owner_uri is None: | ||
raise Exception("Missing owner URI") | ||
|
||
store_path = current_app.config["STORE_PATH"] | ||
WerLaj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
visualization_path = current_app.config["VISUALIZATION_PATH"] | ||
|
||
return PKG( | ||
URI(owner_uri), | ||
RDFStore.MEMORY, | ||
f"{store_path}/{owner_username}", | ||
visualization_path=visualization_path, | ||
) | ||
|
||
|
||
def parse_query_request_data(data: Dict[str, Any]) -> str: | ||
"""Parses the request data to execute SPARQL query. | ||
|
||
Args: | ||
data: Request data. | ||
|
||
Returns: | ||
A string containing SPARQL query. | ||
""" | ||
return data.get("sparql_query", None) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,88 @@ | ||
"""Tests for the pkg exploration endpoints.""" | ||
|
||
import os | ||
|
||
def test_pkg_exploration_endpoint(client) -> None: | ||
"""Test the pkg exploration endpoint.""" | ||
response = client.get("/explore") | ||
from flask import Flask | ||
|
||
|
||
def test_pkg_exploration_endpoint_errors(client: Flask) -> None: | ||
"""Tests /explore endpoints with invalid data.""" | ||
response = client.get( | ||
"/explore", | ||
json={ | ||
"owner_username": "test", | ||
}, | ||
) | ||
assert response.status_code == 400 | ||
assert response.json["message"] == "Missing owner URI" | ||
|
||
response = client.post( | ||
"/explore", | ||
json={ | ||
"owner_username": "test", | ||
}, | ||
) | ||
assert response.status_code == 400 | ||
assert response.json["message"] == "Missing owner URI" | ||
NoB0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
response = client.post( | ||
"/explore", | ||
json={ | ||
"owner_uri": "http://example.com#test", | ||
"owner_username": "test", | ||
"sparql_query": ( | ||
"INSERT DATA { _:st a rdf:Statement ; " | ||
'rdf:predicate [ a skos:Concept ; dc:description "like" ] ;' | ||
"rdf:object" | ||
'[ a skos:Concept ; dc:description "icecream"] . ' | ||
"<http://example.com#test> wi:preference [" | ||
"pav:derivedFrom _:st ;" | ||
'wi:topic [ a skos:Concept ; dc:description "icecream"] ;' | ||
"wo:weight" | ||
"[ wo:weight_value 1.0 ; wo:scale pkg:StandardScale]] . }" | ||
), | ||
}, | ||
) | ||
assert response.status_code == 400 | ||
assert ( | ||
response.json["message"] | ||
== "Operation is not supported. Provide SPARQL select query." | ||
) | ||
|
||
|
||
def test_pkg_visualization(client: Flask) -> None: | ||
"""Tests the GET /explore endpoint.""" | ||
if not os.path.exists("tests/data/pkg_visualizations/"): | ||
os.makedirs("tests/data/pkg_visualizations/", exist_ok=True) | ||
if not os.path.exists("tests/data/RDFStore/"): | ||
os.makedirs("tests/data/RDFStore/", exist_ok=True) | ||
response = client.get( | ||
"/explore", | ||
json={ | ||
"owner_uri": "http://example.com#test", | ||
"owner_username": "test", | ||
}, | ||
) | ||
assert response.status_code == 200 | ||
assert response.json["message"] == "PKG visualized successfully." | ||
assert response.json["img_path"] == "tests/data/pkg_visualizations/test.png" | ||
|
||
|
||
def test_pkg_sparql_query(client: Flask) -> None: | ||
"""Tests the POST /explore endpoint.""" | ||
if not os.path.exists("tests/data/RDFStore/"): | ||
os.makedirs("tests/data/RDFStore/", exist_ok=True) | ||
response = client.post( | ||
"/explore", | ||
json={ | ||
"owner_uri": "http://example.com#test", | ||
"owner_username": "test", | ||
"sparql_query": ( | ||
"SELECT ?statement WHERE { " | ||
"?statement rdf:predicate " | ||
'[ a skos:Concept ; dc:description "like" ] . }' | ||
), | ||
}, | ||
) | ||
assert response.status_code == 200 | ||
assert response.get_json() == {"message": "PKG Exploration"} | ||
assert response.json["message"] == "SPARQL query executed successfully." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we import specific libraries instead of full io and pydotplus?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of pydotplus, I'm not sure. In the implementation, I just follow the example that they provided in the documentation of this package.