Skip to content
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

Merged
merged 34 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
037f3d6
Test with KG visualzation tools
WerLaj Jan 9, 2024
5d82796
Draft impelmentation of backend for visualizing PKG
WerLaj Jan 11, 2024
876d650
Formatting issues
WerLaj Jan 11, 2024
e0ac3cc
Missing package
WerLaj Jan 11, 2024
c6ae304
Missing package
WerLaj Jan 11, 2024
b5d9507
Missing package
WerLaj Jan 11, 2024
f91a565
Missing package
WerLaj Jan 11, 2024
df51de5
Missing package
WerLaj Jan 11, 2024
946d2e7
Missing package
WerLaj Jan 12, 2024
ec7b53a
Missing package
WerLaj Jan 12, 2024
daff0d5
Missing package
WerLaj Jan 12, 2024
a350ae4
Missing package
WerLaj Jan 12, 2024
9f8c163
Missing package
WerLaj Jan 12, 2024
6ad2043
Missing package
WerLaj Jan 12, 2024
c4fd937
Missing package
WerLaj Jan 12, 2024
438c8c3
Missing package
WerLaj Jan 12, 2024
7ff6d05
Missing package
WerLaj Jan 12, 2024
fd3cf4f
Missing package
WerLaj Jan 12, 2024
fb79bd6
Missing package
WerLaj Jan 12, 2024
931e4a0
Missing package
WerLaj Jan 12, 2024
3ead65f
Update docstrings
WerLaj Jan 12, 2024
9ab4cbe
Merge with main
WerLaj Jan 29, 2024
a50e064
Fixed failing tests
WerLaj Jan 29, 2024
1ecb272
Fixed typos
WerLaj Jan 29, 2024
858150d
Fixes after code review
WerLaj Feb 1, 2024
54ec73a
Merge with main
WerLaj Feb 1, 2024
84bd4a4
Fix mypy issues
WerLaj Feb 1, 2024
2cf8ccb
Fixing tests
WerLaj Feb 1, 2024
4cef2ab
Fix mypy issues
WerLaj Feb 1, 2024
1d124ab
Fixes after code review
WerLaj Feb 2, 2024
f27bdb8
Fixes after code review
WerLaj Feb 2, 2024
5777920
merge with main
WerLaj Feb 2, 2024
7f82a76
Fix failing test
WerLaj Feb 2, 2024
f17f23e
Fixes after code review
WerLaj Feb 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
pip install --upgrade pip
pip install -r requirements.txt

- name: Install Graphviz
uses: tlylt/install-graphviz@v1

- name: Run black
shell: bash
run: pre-commit run black --all-files
Expand Down Expand Up @@ -68,6 +71,9 @@ jobs:
pip install --upgrade pip
pip install -r requirements.txt

- name: Install Graphviz
uses: tlylt/install-graphviz@v1

- name: Run mypy
shell: bash
run: pre-commit run mypy --all-file
Expand All @@ -92,6 +98,10 @@ jobs:
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-github-actions-annotate-failures

- name: Install graphviz
uses: tlylt/install-graphviz@v1


- name: PyTest with code coverage
continue-on-error: true
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/merge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-github-actions-annotate-failures

- name: Install Graphviz
uses: tlylt/install-graphviz@v1

- name: PyTest with code coverage
continue-on-error: true
Expand Down
1 change: 1 addition & 0 deletions data/pkg_visualizations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# PKG Visualizations
56 changes: 55 additions & 1 deletion pkg_api/pkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,43 @@
can be found here: https://github.com/iai-group/pkg-vocabulary
"""

import io
from typing import Dict, Optional

import pydotplus

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?

Copy link
Collaborator Author

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.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong indentation (extra spaces)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have error handling?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
6 changes: 6 additions & 0 deletions pkg_api/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from flask import Flask
from flask_restful import Api

from pkg_api.connector import DEFAULT_STORE_PATH
from pkg_api.pkg import DEFAULT_VISUALIZATION_PATH
from pkg_api.server.auth import AuthResource
from pkg_api.server.facts_management import PersonalFactsResource
from pkg_api.server.models import db
Expand All @@ -28,8 +30,12 @@ def create_app(testing: bool = False) -> Flask:
if testing:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///test.sqlite"
app.config["STORE_PATH"] = "tests/data/RDFStore"
app.config["VISUALIZATION_PATH"] = "tests/data/pkg_visualizations"
else:
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite"
app.config["STORE_PATH"] = DEFAULT_STORE_PATH
app.config["VISUALIZATION_PATH"] = DEFAULT_VISUALIZATION_PATH

db.init_app(app)

Expand Down
59 changes: 55 additions & 4 deletions pkg_api/server/pkg_exploration.py
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
45 changes: 45 additions & 0 deletions pkg_api/server/utils.py
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)
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ requests
types-requests
pyyaml
types-pyyaml
pyparsing>=2.4.7
graphviz
pydotplus>=2.0.2
ipython>=7.28.0
ollama
rfc3987
88 changes: 84 additions & 4 deletions tests/pkg_api/server/test_pkg_exploration.py
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."
Loading