feat(core): refactor Neo4jDict to GraphDict

This commit is contained in:
dextmorgn
2026-01-22 09:41:01 +01:00
parent d0343cba50
commit 3c6a23ebfa
8 changed files with 95 additions and 100 deletions
+51 -56
View File
@@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@plugin "tailwindcss-animate";
@@ -70,8 +70,8 @@ body::-webkit-scrollbar-thumb:hover {
body {
@apply m-0;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: var(--tracking-normal);
@@ -147,7 +147,6 @@ html {
@apply rounded border border-border bg-[var(--muted)] px-1 py-0.5 text-sm;
}
.minimal-tiptap-editor .ProseMirror pre {
@apply relative overflow-auto rounded font-mono text-sm;
@apply border-[var(--mt-pre-border)] bg-[var(--mt-pre-background)] text-[var(--mt-pre-color)];
@@ -163,7 +162,6 @@ html {
}
.minimal-tiptap-editor .ProseMirror pre {
.hljs-keyword,
.hljs-operator,
.hljs-function,
@@ -256,7 +254,7 @@ html {
list-style: square;
}
.minimal-tiptap-editor .ProseMirror>p.is-editor-empty::before {
.minimal-tiptap-editor .ProseMirror > p.is-editor-empty::before {
content: attr(data-placeholder);
@apply pointer-events-none float-left h-0 text-[var(--mt-secondary)];
}
@@ -344,10 +342,10 @@ html {
z-index: 1;
}
[data-rmiz-content="found"] img,
[data-rmiz-content="found"] svg,
[data-rmiz-content="found"] [role="img"],
[data-rmiz-content="found"] [data-zoom] {
[data-rmiz-content='found'] img,
[data-rmiz-content='found'] svg,
[data-rmiz-content='found'] [role='img'],
[data-rmiz-content='found'] [data-zoom] {
cursor: inherit;
}
@@ -376,11 +374,11 @@ html {
transition: background-color 0.3s;
}
[data-rmiz-modal-overlay="hidden"] {
[data-rmiz-modal-overlay='hidden'] {
background-color: rgba(255, 255, 255, 0);
}
[data-rmiz-modal-overlay="visible"] {
[data-rmiz-modal-overlay='visible'] {
background-color: rgba(255, 255, 255, 1);
}
@@ -399,7 +397,6 @@ html {
}
@media (prefers-reduced-motion: reduce) {
[data-rmiz-modal-overlay],
[data-rmiz-modal-img] {
transition-duration: 0.01ms !important;
@@ -407,49 +404,49 @@ html {
}
:root {
--background: oklch(.99 0 0);
--background: oklch(0.99 0 0);
--foreground: oklch(0.27 0 0);
--card: oklch(1.40 0 0);
--card: oklch(1.4 0 0);
--card-foreground: oklch(0.27 0 0);
--popover: oklch(21.00 0 0);
--popover: oklch(21 0 0);
--popover-foreground: oklch(0.27 0 0);
--primary: oklch(0.6397 0.1720 36.4421);
--primary: oklch(0.6397 0.172 36.4421);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.97 0.00 264.54);
--secondary-foreground: oklch(0.45 0.03 256.80);
--muted: oklch(0.98 0.00 247.84);
--secondary: oklch(0.97 0 264.54);
--secondary-foreground: oklch(0.45 0.03 256.8);
--muted: oklch(0.98 0 247.84);
--muted-foreground: oklch(0.55 0.02 264.36);
--accent: oklch(0.99 0.02 95.28);
--accent-foreground: oklch(0.47 0.12 46.20);
--accent-foreground: oklch(0.47 0.12 46.2);
--destructive: oklch(0.64 0.21 25.33);
--destructive-foreground: oklch(1.00 0 0);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.93 0.01 264.53);
--input: oklch(0.93 0.01 264.53);
--ring: oklch(0.77 0.16 70.08);
--chart-1: oklch(0.77 0.16 70.08);
--chart-2: oklch(0.67 0.16 58.32);
--chart-3: oklch(0.56 0.15 49.00);
--chart-4: oklch(0.47 0.12 46.20);
--chart-5: oklch(0.41 0.11 45.90);
--sidebar: oklch(0.98 0.00 247.84);
--chart-3: oklch(0.56 0.15 49);
--chart-4: oklch(0.47 0.12 46.2);
--chart-5: oklch(0.41 0.11 45.9);
--sidebar: oklch(0.98 0 247.84);
--sidebar-foreground: oklch(0.27 0 0);
--sidebar-primary: oklch(0.77 0.16 70.08);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.99 0.02 95.28);
--sidebar-accent-foreground: oklch(0.47 0.12 46.20);
--sidebar-accent-foreground: oklch(0.47 0.12 46.2);
--sidebar-border: oklch(0.93 0.01 264.53);
--sidebar-ring: oklch(0.77 0.16 70.08);
--font-sans: Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.580rem;
--radius: 0.58rem;
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
--success: oklch(0.6 0.12 145);
--warning: oklch(0.7 0.12 80);
@@ -466,43 +463,43 @@ html {
--card-foreground: oklch(0.92 0 0);
--popover: oklch(0.27 0 0);
--popover-foreground: oklch(0.92 0 0);
--primary: oklch(0.6397 0.1720 36.4421);
--primary: oklch(0.6397 0.172 36.4421);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.27 0 0);
--secondary-foreground: oklch(0.92 0 0);
--muted: oklch(0.27 0 0);
--muted-foreground: oklch(0.72 0 0);
--accent: oklch(0.47 0.12 46.20);
--accent: oklch(0.47 0.12 46.2);
--accent-foreground: oklch(0.92 0.12 95.75);
--destructive: oklch(0.64 0.21 25.33);
--destructive-foreground: oklch(1.00 0 0);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.29 0 0);
--input: oklch(0.37 0 0);
--ring: oklch(0.77 0.16 70.08);
--chart-1: oklch(0.84 0.16 84.43);
--chart-2: oklch(0.67 0.16 58.32);
--chart-3: oklch(0.47 0.12 46.20);
--chart-4: oklch(0.56 0.15 49.00);
--chart-5: oklch(0.47 0.12 46.20);
--chart-3: oklch(0.47 0.12 46.2);
--chart-4: oklch(0.56 0.15 49);
--chart-5: oklch(0.47 0.12 46.2);
--sidebar: oklch(0.17 0 0);
--sidebar-foreground: oklch(0.92 0 0);
--sidebar-primary: oklch(0.77 0.16 70.08);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.47 0.12 46.20);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.47 0.12 46.2);
--sidebar-accent-foreground: oklch(0.92 0.12 95.75);
--sidebar-border: oklch(0.37 0 0);
--sidebar-ring: oklch(0.77 0.16 70.08);
--font-sans: Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.580rem;
--radius: 0.58rem;
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
}
@@ -571,7 +568,7 @@ html {
@apply z-0 outline-0;
}
.minimal-tiptap-editor .ProseMirror>div.editor {
.minimal-tiptap-editor .ProseMirror > div.editor {
@apply block flex-1 whitespace-pre-wrap;
}
@@ -604,8 +601,8 @@ html {
}
.minimal-tiptap-editor .ProseMirror li .text-node:has(+ .list-node),
.minimal-tiptap-editor .ProseMirror li>.list-node,
.minimal-tiptap-editor .ProseMirror li>.text-node,
.minimal-tiptap-editor .ProseMirror li > .list-node,
.minimal-tiptap-editor .ProseMirror li > .text-node,
.minimal-tiptap-editor .ProseMirror li p {
@apply mb-0;
}
@@ -658,7 +655,7 @@ html {
}
/* Basic editor styles */
.tiptap> :first-child {
.tiptap > :first-child {
margin-top: 0;
}
@@ -761,7 +758,7 @@ th,
opacity: 0;
}
*:hover>.resizer {
*:hover > .resizer {
opacity: 1;
}
}
@@ -795,7 +792,6 @@ th,
}
.hide-scrollbar {
/* Webkit browsers (Chrome, Safari, Edge) */
-webkit-scrollbar {
display: none;
@@ -826,7 +822,6 @@ th,
transform-origin: bottom right;
}
body {
letter-spacing: var(--tracking-normal);
}
@@ -854,4 +849,4 @@ body {
width: 100%;
transform-origin: top left;
touch-action: none;
}
}
@@ -12,12 +12,12 @@ from .connection import Neo4jConnection, neo4j_connection
from .repository import Neo4jGraphRepository
from .repository_protocol import GraphRepositoryProtocol
from .serializer import GraphSerializer
from .service import GraphService, create_graph_service, LoggerProtocol
from .service import GraphService, LoggerProtocol, create_graph_service
from .types import (
GraphData,
GraphDict,
GraphEdge,
GraphNode,
Neo4jDict,
NodeMetadata,
)
@@ -38,6 +38,6 @@ __all__ = [
"GraphData",
"GraphEdge",
"GraphNode",
"Neo4jDict",
"GraphDict",
"NodeMetadata",
]
@@ -2,14 +2,14 @@
Graph database repository for Neo4j operations.
This module provides a repository pattern implementation for Neo4j,
handling raw Neo4jDict object and operations with batching support.
handling raw GraphDict object and operations with batching support.
"""
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from .connection import Neo4jConnection
from .types import Neo4jDict
from .types import GraphDict
class Neo4jGraphRepository:
@@ -36,17 +36,17 @@ class Neo4jGraphRepository:
def create_node(
self,
node_obj: Neo4jDict,
node_obj: GraphDict,
sketch_id: str,
) -> Optional[str]:
"""
Create or update a single node in Neo4j.
Supported signature:
1. Neo4jDict object: create_node(obj)
1. GraphDict object: create_node(obj)
Args:
node_obj: a Neo4jDict object
node_obj: a GraphDict object
sketch_id: str
Returns:
@@ -61,7 +61,7 @@ class Neo4jGraphRepository:
def create_relationship(
self,
rel_obj: Neo4jDict,
rel_obj: GraphDict,
sketch_id: str,
) -> None:
"""
@@ -71,7 +71,7 @@ class Neo4jGraphRepository:
- Pydantic objects: create_relationship(relation, sketch_id="...")
Args:
rel_obj: the Neo4jDict object
rel_obj: the GraphDict object
sketch_id: Investigation sketch ID (required)
"""
if not self._connection:
@@ -103,7 +103,7 @@ class Neo4jGraphRepository:
self.flush_batch()
def _build_node_query(
self, node_obj: Neo4jDict, sketch_id: str
self, node_obj: GraphDict, sketch_id: str
) -> Tuple[str, Dict[str, Any]]:
node_label = node_obj.get("nodeLabel")
node_type = node_obj.get("nodeType")
@@ -127,7 +127,7 @@ class Neo4jGraphRepository:
def _build_relationship_query(
self,
rel_obj: Neo4jDict,
rel_obj: GraphDict,
sketch_id: str,
) -> Tuple[str, Dict[str, Any]]:
from_type = rel_obj["from_type"]
@@ -183,14 +183,14 @@ class Neo4jGraphRepository:
def batch_create_nodes(
self,
nodes: List[Neo4jDict],
nodes: List[GraphDict],
sketch_id: str,
) -> Dict[str, Any]:
"""
Create multiple nodes in a single batch transaction.
Args:
nodes: List of Neo4jDict model instances to insert
nodes: List of GraphDict model instances to insert
sketch_id: investigation sketch id
Returns:
@@ -256,7 +256,7 @@ class Neo4jGraphRepository:
Args:
edges: List of edge dictionaries, each with:
- rel_obj: Source node Neo4jDict model
- rel_obj: Source node GraphDict model
Returns:
Dictionary with:
@@ -300,9 +300,9 @@ class Neo4jGraphRepository:
def batch_create_edges_by_element_id(
self,
edges: List[Neo4jDict],
edges: List[GraphDict],
sketch_id: str,
) -> Neo4jDict:
) -> GraphDict:
"""
Create multiple edges/relationships using element IDs in a single batch transaction.
@@ -388,7 +388,7 @@ class Neo4jGraphRepository:
return {"edges_created": 0, "errors": errors}
def update_node(
self, element_id: str, updates: Neo4jDict, sketch_id: str
self, element_id: str, updates: GraphDict, sketch_id: str
) -> Optional[str]:
if not self._connection:
return None
@@ -532,7 +532,7 @@ class Neo4jGraphRepository:
return {"nodes": nodes_result, "edges": rels_result or []}
def update_relationship(
self, element_id: str, rel_obj: Neo4jDict, sketch_id: str
self, element_id: str, rel_obj: GraphDict, sketch_id: str
) -> Optional[Dict[str, Any]]:
if not self._connection:
return None
@@ -5,9 +5,9 @@ This module defines the interface contract that all graph repository
implementations must follow, enabling dependency injection and easier testing.
"""
from typing import Any, Dict, List, Optional, Protocol, Tuple
from typing import Any, Dict, List, Optional, Protocol
from .types import Neo4jDict
from .types import GraphDict
class GraphRepositoryProtocol(Protocol):
@@ -21,12 +21,12 @@ class GraphRepositoryProtocol(Protocol):
"""
# Core node operations
def create_node(self, node_obj: Neo4jDict, sketch_id: str) -> Optional[str]:
def create_node(self, node_obj: GraphDict, sketch_id: str) -> Optional[str]:
"""Create or update a single node. Returns element ID."""
...
def update_node(
self, element_id: str, updates: Neo4jDict, sketch_id: str
self, element_id: str, updates: GraphDict, sketch_id: str
) -> Optional[str]:
"""Update a node by its element ID. Returns element ID."""
...
@@ -52,7 +52,7 @@ class GraphRepositoryProtocol(Protocol):
...
# Core relationship operations
def create_relationship(self, rel_obj: Neo4jDict, sketch_id: str) -> None:
def create_relationship(self, rel_obj: GraphDict, sketch_id: str) -> None:
"""Create a relationship between two nodes."""
...
@@ -67,7 +67,7 @@ class GraphRepositoryProtocol(Protocol):
...
def update_relationship(
self, element_id: str, rel_obj: Neo4jDict, sketch_id: str
self, element_id: str, rel_obj: GraphDict, sketch_id: str
) -> Optional[Dict[str, Any]]:
"""Update a relationship by its element ID."""
...
@@ -100,13 +100,13 @@ class GraphRepositoryProtocol(Protocol):
# Batch operations
def batch_create_nodes(
self, nodes: List[Neo4jDict], sketch_id: str
self, nodes: List[GraphDict], sketch_id: str
) -> Dict[str, Any]:
"""Create multiple nodes in a single batch."""
...
def batch_create_edges_by_element_id(
self, edges: List[Neo4jDict], sketch_id: str
self, edges: List[GraphDict], sketch_id: str
) -> Dict[str, Any]:
"""Create multiple edges using element IDs in a single batch."""
...
@@ -13,7 +13,7 @@ from pydantic import BaseModel
from .repository import Neo4jGraphRepository
from .repository_protocol import GraphRepositoryProtocol
from .serializer import GraphSerializer
from .types import GraphData, GraphNode, Neo4jDict
from .types import GraphData, GraphDict, GraphNode
class LoggerProtocol(Protocol):
@@ -90,7 +90,7 @@ class GraphService:
"create_node method takes a GraphNode as input. If you want to insert a node from a FlowsintType, please use create_node_from_flowsint_type method."
)
neo4j_node_dict: Neo4jDict = GraphSerializer.graph_node_to_neo4j_dict(node_obj)
neo4j_node_dict: GraphDict = GraphSerializer.graph_node_to_neo4j_dict(node_obj)
if self._enable_batching:
self._repository.add_to_batch(
@@ -120,7 +120,7 @@ class GraphService:
"create_node_from_flowsint_type method takes a FlowsintType as input. If you want to insert a node from a GraphNode, please use create_node method."
)
neo4j_node_dict: Neo4jDict = GraphSerializer.flowsint_type_to_neo4j_dict(
neo4j_node_dict: GraphDict = GraphSerializer.flowsint_type_to_neo4j_dict(
node_obj
)
@@ -169,7 +169,7 @@ class GraphService:
**properties: Additional relationship properties
"""
neo4j_rel_dict: Neo4jDict = GraphSerializer.graph_edge_to_neo4j_dict(
neo4j_rel_dict: GraphDict = GraphSerializer.graph_edge_to_neo4j_dict(
from_obj, to_obj, rel_label
)
@@ -267,7 +267,7 @@ class GraphService:
sketch_id=self._sketch_id,
)
def batch_create_nodes(self, nodes: List[Neo4jDict]) -> Dict[str, Any]:
def batch_create_nodes(self, nodes: List[GraphDict]) -> Dict[str, Any]:
"""Create multiple nodes in a single batch transaction."""
return self._repository.batch_create_nodes(
nodes=nodes,
@@ -275,7 +275,7 @@ class GraphService:
)
def batch_create_edges_by_element_id(
self, edges: List[Neo4jDict]
self, edges: List[GraphDict]
) -> Dict[str, Any]:
"""Create multiple edges using element IDs in a single batch transaction."""
return self._repository.batch_create_edges_by_element_id(
@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union
from flowsint_types import FlowsintType
from pydantic import BaseModel, Field
Neo4jDict = Dict[str, Any]
GraphDict = Dict[str, Any]
class NodeMetadata(BaseModel):
@@ -36,7 +36,7 @@ class GraphNode(BaseModel):
nodeFlag: Optional[str] = None
nodeMetadata: NodeMetadata
nodeProperties: FlowsintType
nodeProperties: Any
x: Optional[float] = 100.0
y: Optional[float] = 100.0
@@ -71,9 +71,9 @@ def _get_nodes(nodes: List[Dict]) -> List[EntityPreview]:
results = []
for node in nodes:
node_id = node.get("id")
node_type = node.get("type", None)
label = node.get("label", None)
node_obj = {"type": node_type, "label": label, **node}
node_type = node.get("nodeType", node.get("type"))
label = node.get("nodeLabel", node.get("label"))
node_obj = {"nodeType": node_type, "nodeLabel": label, **node}
preview = _parse_node(node_obj)
if preview:
results.append(
@@ -88,10 +88,10 @@ def _get_nodes(nodes: List[Dict]) -> List[EntityPreview]:
def _parse_node(nodeDict: dict) -> EntityPreview | None:
type = nodeDict.get("type")
type = nodeDict.get("nodeType")
first_key = next(iter(nodeDict), None)
label = (
nodeDict.get("label")
nodeDict.get("nodeLabel")
or (nodeDict.get(first_key) if first_key else None)
or nodeDict.get("id")
)
@@ -4,9 +4,9 @@ import pytest
from flowsint_types import Domain, Ip
from flowsint_core.core.graph import (
GraphSerializer,
GraphEdge,
GraphNode,
GraphSerializer,
NodeMetadata,
)
@@ -184,7 +184,7 @@ class TestGraphNodeToFlowsintType:
assert result.domain == "example.com"
class TestNeo4jDictToGraphEdge:
class TestGraphDictToGraphEdge:
def test_converts_edge_dict(self):
edge_dict = {
"id": "edge-123",
@@ -207,7 +207,7 @@ class TestNeo4jDictToGraphEdge:
assert result.target == "2"
class TestGraphEdgeToNeo4jDict:
class TestGraphEdgeToGraphDict:
def test_with_flowsint_types(self):
from_obj = Domain(domain="source.com")
to_obj = Domain(domain="target.com")
@@ -313,7 +313,7 @@ class TestDeserializeEdges:
assert result == []
class TestNeo4jDictToGraphNodeErrors:
class TestGraphDictToGraphNodeErrors:
def test_raises_on_missing_data(self):
node_dict = {"id": "123"}
with pytest.raises(Exception, match="Could not find node data"):