Skip to content

Commit 5f42779

Browse files
authored
feat: Add edge attributes fetching and schema retrieval (#52)
1 parent 9cf6b90 commit 5f42779

File tree

4 files changed

+119
-9
lines changed

4 files changed

+119
-9
lines changed

motifstudio-web/src/app/GraphStats.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ export function GraphStats({
5858
host_id: graph?.id,
5959
})
6060
);
61+
const {
62+
data: edgeAttrData,
63+
error: edgeAttrError,
64+
isLoading: edgeAttrIsLoading,
65+
} = useSWR<{
66+
attributes: { [key: string]: string };
67+
}>([`${BASE_URL}/queries/edges/attributes`, graph?.id], () =>
68+
bodiedFetcher(`${BASE_URL}/queries/edges/attributes`, {
69+
host_id: graph?.id,
70+
})
71+
);
6172

6273
// To handle the fact that the attributes are loaded asynchronously, we
6374
// provide a callback function to the parent component to share the
@@ -70,8 +81,8 @@ export function GraphStats({
7081

7182
// Use client-only check to avoid hydration mismatch
7283
if (!isClient) return <div>Loading...</div>;
73-
if (vertIsLoading || edgeIsLoading) return <div>Loading...</div>;
74-
if (vertError || edgeError) return <div>Error: {vertError}</div>;
84+
if (vertIsLoading || edgeIsLoading || vertAttrIsLoading || edgeAttrIsLoading) return <div>Loading...</div>;
85+
if (vertError || edgeError || vertAttrError || edgeAttrError) return <div>Error: {vertError || edgeError || vertAttrError || edgeAttrError}</div>;
7586
if (!vertData || !edgeData) return <div>No data</div>;
7687

7788
/**
@@ -153,13 +164,30 @@ export function GraphStats({
153164
<div className="flex gap-2">
154165
{vertAttrData?.attributes
155166
? Object.entries(vertAttrData?.attributes).map(([key, value]) => (
156-
<span
157-
key={key}
158-
className="px-2 py-1 bg-blue-50 rounded-md shadow-sm text-sm font-medium text-blue-800"
159-
>
160-
{key} <b className="font-mono">({value})</b>
161-
</span>
162-
))
167+
<span
168+
key={key}
169+
className="px-2 py-1 bg-blue-50 rounded-md shadow-sm text-sm font-medium text-blue-800"
170+
>
171+
{key} <b className="font-mono">({value})</b>
172+
</span>
173+
))
174+
: null}
175+
</div>
176+
177+
<hr className="my-2 w-full" />
178+
179+
{/* Edge attributes list */}
180+
<h3 className="text-lg font-mono w-full">Edge Attributes</h3>
181+
<div className="flex gap-2">
182+
{edgeAttrData?.attributes
183+
? Object.entries(edgeAttrData?.attributes).map(([key, value]) => (
184+
<span
185+
key={key}
186+
className="px-2 py-1 bg-green-50 rounded-md shadow-sm text-sm font-medium text-green-800"
187+
>
188+
{key} <b className="font-mono">({value})</b>
189+
</span>
190+
))
163191
: null}
164192
</div>
165193

server/src/host_provider/host_provider/host_provider.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ def get_edge_count(self, uri: str) -> int:
7272
"""
7373
...
7474

75+
def get_edge_attribute_schema(self, uri: str) -> AttributeSchema:
76+
"""Return the schema of the edge attributes in the graph.
77+
78+
Arguments:
79+
uri (str): The URI of the host.
80+
81+
Returns:
82+
dict[str, str]: The schema of the edge attributes in the graph.
83+
84+
"""
85+
...
86+
7587
def get_motif_count(self, uri: str, motif_string: str) -> int:
7688
"""Get a count of motifs in the graph.
7789
@@ -175,6 +187,31 @@ def get_edge_count(self, uri: str) -> int:
175187
"""
176188
return len(self.get_networkx_graph(uri).edges)
177189

190+
def get_edge_attribute_schema(self, uri: str) -> dict[str, str]:
191+
"""Return the schema of the edge attributes in the graph.
192+
193+
Arguments:
194+
uri (str): The URI of the host.
195+
196+
Returns:
197+
dict[str, str]: The schema of the edge attributes in the graph.
198+
199+
"""
200+
g = self.get_networkx_graph(uri)
201+
# TODO: This exhaustive search is no good for very large graphs.
202+
203+
# Detect types of attributes, which may be different for different
204+
# edges.
205+
attribute_types = {}
206+
for edge in g.edges:
207+
for attribute in g.edges[edge].keys():
208+
if attribute not in attribute_types:
209+
attribute_types[attribute] = type(g.edges[edge][attribute]).__name__
210+
else:
211+
if attribute_types[attribute] != type(g.edges[edge][attribute]).__name__:
212+
attribute_types[attribute] = "str"
213+
return attribute_types
214+
178215
def get_motif_count(self, uri: str, motif_string: str) -> int:
179216
"""Count the number of instances of a motif in the graph.
180217

server/src/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ class EdgeCountQueryResponse(_QueryResponseBase):
9898
edge_count: int
9999

100100

101+
class EdgeAttributeQueryRequest(_QueryRequestBase):
102+
"""A request to get the edge attributes for a host graph."""
103+
104+
...
105+
106+
107+
class EdgeAttributeQueryResponse(_QueryResponseBase):
108+
"""A response with the edge attribute results for a host graph."""
109+
110+
# Attribute name to attribute schema:
111+
attributes: AttributeSchema
112+
113+
101114
class MotifParseQueryRequest(_QueryRequestBase):
102115
"""A request to parse a motif query."""
103116

server/src/server/routers/queries.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from ...models import (
1414
EdgeCountQueryRequest,
1515
EdgeCountQueryResponse,
16+
EdgeAttributeQueryRequest,
17+
EdgeAttributeQueryResponse,
1618
MotifCountQueryRequest,
1719
MotifCountQueryResponse,
1820
MotifParseQueryRequest,
@@ -222,6 +224,36 @@ def query_count_edges(
222224
)
223225

224226

227+
@router.post("/edges/attributes")
228+
def query_edge_attributes(
229+
edge_attribute_query_request: EdgeAttributeQueryRequest,
230+
commons: Annotated[HostProviderRouterGlobalDep, Depends(provider_router)],
231+
) -> EdgeAttributeQueryResponse:
232+
"""Get the edge attributes for a given host."""
233+
tic = time.time()
234+
uri = commons.get_uri_from_id(edge_attribute_query_request.host_id)
235+
if uri is None:
236+
raise HTTPException(
237+
status_code=404,
238+
detail=f"No host found with ID {edge_attribute_query_request.host_id}",
239+
)
240+
241+
provider = commons.host_provider_router.provider_for(uri)
242+
if provider is None:
243+
raise HTTPException(
244+
status_code=404,
245+
detail=f"No provider found for host {edge_attribute_query_request.host_id}",
246+
)
247+
248+
attributes = provider.get_edge_attribute_schema(uri)
249+
return EdgeAttributeQueryResponse(
250+
attributes=attributes,
251+
host_id=edge_attribute_query_request.host_id,
252+
response_time=datetime.datetime.now().isoformat(),
253+
response_duration_ms=(time.time() - tic) * 1000,
254+
)
255+
256+
225257
@router.post("/motifs/count")
226258
def query_count_motifs(
227259
motif_count_query_request: MotifCountQueryRequest,

0 commit comments

Comments
 (0)