@@ -85,6 +85,54 @@ def __repr__(self) -> str:
8585 return f"VectorResult(distance={ self .distance :.4f} , node={ self .node } )"
8686
8787
88+ class PropertyDefinitionInfo :
89+ """A property definition from the schema (name, type, required, unique)."""
90+
91+ def __init__ (self , proto_def : Any ) -> None :
92+ self .name : str = proto_def .name
93+ self .type : int = proto_def .type
94+ self .required : bool = proto_def .required
95+ self .unique : bool = proto_def .unique
96+
97+ def __repr__ (self ) -> str :
98+ return f"PropertyDefinitionInfo(name={ self .name !r} , type={ self .type } , required={ self .required } , unique={ self .unique } )"
99+
100+
101+ class LabelInfo :
102+ """A node label returned from the schema registry."""
103+
104+ def __init__ (self , proto_label : Any ) -> None :
105+ self .name : str = proto_label .name
106+ self .version : int = proto_label .version
107+ self .properties : list [PropertyDefinitionInfo ] = [PropertyDefinitionInfo (p ) for p in proto_label .properties ]
108+
109+ def __repr__ (self ) -> str :
110+ return f"LabelInfo(name={ self .name !r} , version={ self .version } , properties={ len (self .properties )} )"
111+
112+
113+ class EdgeTypeInfo :
114+ """An edge type returned from the schema registry."""
115+
116+ def __init__ (self , proto_edge_type : Any ) -> None :
117+ self .name : str = proto_edge_type .name
118+ self .version : int = proto_edge_type .version
119+ self .properties : list [PropertyDefinitionInfo ] = [PropertyDefinitionInfo (p ) for p in proto_edge_type .properties ]
120+
121+ def __repr__ (self ) -> str :
122+ return f"EdgeTypeInfo(name={ self .name !r} , version={ self .version } , properties={ len (self .properties )} )"
123+
124+
125+ class TraverseResult :
126+ """Result of a graph traversal: reached nodes and traversed edges."""
127+
128+ def __init__ (self , proto_response : Any ) -> None :
129+ self .nodes : list [NodeResult ] = [NodeResult (n ) for n in proto_response .nodes ]
130+ self .edges : list [EdgeResult ] = [EdgeResult (e ) for e in proto_response .edges ]
131+
132+ def __repr__ (self ) -> str :
133+ return f"TraverseResult(nodes={ len (self .nodes )} , edges={ len (self .edges )} )"
134+
135+
88136# ── Async client ─────────────────────────────────────────────────────────────
89137
90138
@@ -303,6 +351,72 @@ async def get_schema_text(self) -> str:
303351
304352 return "\n " .join (lines )
305353
354+ async def get_labels (self ) -> list [LabelInfo ]:
355+ """Return all node labels defined in the schema."""
356+ from coordinode ._proto .coordinode .v1 .graph .schema_pb2 import ListLabelsRequest # type: ignore[import]
357+
358+ resp = await self ._schema_stub .ListLabels (ListLabelsRequest (), timeout = self ._timeout )
359+ return [LabelInfo (label ) for label in resp .labels ]
360+
361+ async def get_edge_types (self ) -> list [EdgeTypeInfo ]:
362+ """Return all edge types defined in the schema."""
363+ from coordinode ._proto .coordinode .v1 .graph .schema_pb2 import ListEdgeTypesRequest # type: ignore[import]
364+
365+ resp = await self ._schema_stub .ListEdgeTypes (ListEdgeTypesRequest (), timeout = self ._timeout )
366+ return [EdgeTypeInfo (et ) for et in resp .edge_types ]
367+
368+ async def traverse (
369+ self ,
370+ start_node_id : int ,
371+ edge_type : str ,
372+ direction : str = "outbound" ,
373+ max_depth : int = 1 ,
374+ ) -> TraverseResult :
375+ """Traverse the graph from *start_node_id* following *edge_type* edges.
376+
377+ Args:
378+ start_node_id: ID of the node to start from.
379+ edge_type: Edge type label to follow (e.g. ``"KNOWS"``).
380+ direction: ``"outbound"`` (default), ``"inbound"``, or ``"both"``.
381+ max_depth: Maximum hop count (default 1).
382+
383+ Returns:
384+ :class:`TraverseResult` with ``nodes`` and ``edges`` lists.
385+ """
386+ # Validate pure string/int inputs before importing proto stubs — ensures ValueError
387+ # is raised even when proto stubs have not been generated yet.
388+ # Type guards come first so that wrong types raise ValueError, not AttributeError/TypeError.
389+ if not isinstance (direction , str ):
390+ raise ValueError (f"direction must be a str, got { type (direction ).__name__ !r} ." )
391+ _valid_directions = {"outbound" , "inbound" , "both" }
392+ key = direction .lower ()
393+ if key not in _valid_directions :
394+ raise ValueError (f"Invalid direction { direction !r} . Must be one of: 'outbound', 'inbound', 'both'." )
395+ # bool is a subclass of int in Python, so `isinstance(True, int)` is True — exclude it.
396+ if not isinstance (max_depth , int ) or isinstance (max_depth , bool ) or max_depth < 1 :
397+ raise ValueError (f"max_depth must be an integer >= 1, got { max_depth !r} ." )
398+
399+ from coordinode ._proto .coordinode .v1 .graph .graph_pb2 import ( # type: ignore[import]
400+ TraversalDirection ,
401+ TraverseRequest ,
402+ )
403+
404+ _direction_map = {
405+ "outbound" : TraversalDirection .TRAVERSAL_DIRECTION_OUTBOUND ,
406+ "inbound" : TraversalDirection .TRAVERSAL_DIRECTION_INBOUND ,
407+ "both" : TraversalDirection .TRAVERSAL_DIRECTION_BOTH ,
408+ }
409+ direction_value = _direction_map [key ]
410+
411+ req = TraverseRequest (
412+ start_node_id = start_node_id ,
413+ edge_type = edge_type ,
414+ direction = direction_value ,
415+ max_depth = max_depth ,
416+ )
417+ resp = await self ._graph_stub .Traverse (req , timeout = self ._timeout )
418+ return TraverseResult (resp )
419+
306420 async def health (self ) -> bool :
307421 from coordinode ._proto .coordinode .v1 .health .health_pb2 import ( # type: ignore[import]
308422 HealthCheckRequest ,
@@ -422,6 +536,24 @@ def create_edge(
422536 def get_schema_text (self ) -> str :
423537 return self ._run (self ._async .get_schema_text ())
424538
539+ def get_labels (self ) -> list [LabelInfo ]:
540+ """Return all node labels defined in the schema."""
541+ return self ._run (self ._async .get_labels ())
542+
543+ def get_edge_types (self ) -> list [EdgeTypeInfo ]:
544+ """Return all edge types defined in the schema."""
545+ return self ._run (self ._async .get_edge_types ())
546+
547+ def traverse (
548+ self ,
549+ start_node_id : int ,
550+ edge_type : str ,
551+ direction : str = "outbound" ,
552+ max_depth : int = 1 ,
553+ ) -> TraverseResult :
554+ """Traverse the graph from *start_node_id* following *edge_type* edges."""
555+ return self ._run (self ._async .traverse (start_node_id , edge_type , direction , max_depth ))
556+
425557 def health (self ) -> bool :
426558 return self ._run (self ._async .health ())
427559
0 commit comments