15from collections.abc
import Sequence
as ABCSequence
16from dataclasses
import dataclass
17from datetime
import date
18from types
import UnionType
19from typing
import Iterable
30from pydantic.fields
import FieldInfo
31from typing_extensions
import (
68 model_config = ConfigDict(revalidate_instances=
"always")
70 def get_element_id(self) -> Optional[int]: ...
73 def get_type(cls) -> str: ...
76 def get_labels(cls) -> Sequence[str]: ...
78 def get_properties(self) -> Mapping[str, Any]:
79 return get_props(self)
81 def get_prop(self, key: str) -> tuple[str, BasicTargetType]:
82 return get_prop(self, key)
85 def get_primary_keys(cls) -> List[str]: ...
87 def get_primary_kv(self) -> Dict[str, BasicTargetType]:
91 def from_primitive(cls, data: NodePrimitive) -> Self: ...
94 def to_type(cls) -> NodeType: ...
98 model_config = ConfigDict(revalidate_instances=
"always")
100 def get_src_element_id(self) -> Optional[int]: ...
102 def get_dst_element_id(self) -> Optional[int]: ...
105 """rank == 0 means not given by input,
107 rank == None means no multiedge key specified by the edge type"""
111 def get_type(cls) -> str: ...
114 def get_src_type(cls) -> str: ...
117 def get_dst_type(cls) -> str: ...
120 def get_labels(cls) -> Sequence[str]: ...
122 def get_properties(self) -> Mapping[str, Any]:
123 return get_props(self)
125 def get_prop(self, key: str) -> tuple[str, BasicTargetType]:
126 return get_prop(self, key)
129 def get_multiedge_keys(cls) -> List[str]: ...
131 def get_multiedge_kv(self) -> Dict[str, BasicTargetType]:
135 def from_primitive(cls, data: EdgePrimitive) -> Self: ...
138 def to_type(cls) -> EdgeType: ...
143 primary_key: Union[str, List[str]],
144 labels: Optional[Sequence[str]] =
None,
146 """Dynamically create a Node type based on the given type and labels"""
149 if isinstance(primary_key, str):
150 primary_key = [primary_key]
154 nebula_type__type__: ClassVar[str] = type
155 nebula_type__primary_key__: ClassVar[List[str]] = primary_key
156 nebula_type__labels__: ClassVar[Sequence[str]] = labels
159 nebula_obj__element_id__: Optional[int] = Field(default=
None, exclude=
True)
162 def from_primitive(cls, data: NodePrimitive) -> Self:
163 return cls.model_validate(
165 "nebula_obj__element_id__": data[
"id"],
166 **data[
"properties"],
170 def get_element_id(self):
171 return self.nebula_obj__element_id__
175 return cls.nebula_type__type__
179 return cls.nebula_type__labels__
182 def get_primary_keys(cls):
183 return cls.nebula_type__primary_key__
187 return node_model_to_type(cls)
189 return NodeModelDynamic
196 labels: Optional[Sequence[str]] =
None,
197 multiedge_keys: Optional[List[str]] =
None,
199 """Dynamically create an Edge type based on the given type information"""
202 multiedge_keys = multiedge_keys
or []
206 nebula_type__src_type__: ClassVar[str] = src_type
207 nebula_type__edge_type__: ClassVar[str] = edge_type
208 nebula_type__dst_type__: ClassVar[str] = dst_type
209 nebula_type__labels__: ClassVar[Sequence[str]] = labels
210 nebula_type__multiedge_key__: ClassVar[List[str]] = multiedge_keys
213 nebula_obj__src_element_id__: Optional[int] = Field(default=
None, exclude=
True)
214 nebula_obj__dst_element_id__: Optional[int] = Field(default=
None, exclude=
True)
215 nebula_obj__rank__: Optional[int] = Field(default=
None, exclude=
True)
218 def from_primitive(cls, data: EdgePrimitive) -> Self:
219 return cls.model_validate(
221 "nebula_obj__src_element_id__": data[
"src_id"],
222 "nebula_obj__dst_element_id__": data[
"dst_id"],
223 "nebula_obj__rank__": data[
"rank"],
224 **data[
"properties"],
228 def get_src_element_id(self):
229 return self.nebula_obj__src_element_id__
231 def get_dst_element_id(self):
232 return self.nebula_obj__dst_element_id__
235 return self.nebula_obj__rank__
239 return cls.nebula_type__edge_type__
242 def get_src_type(cls):
243 return cls.nebula_type__src_type__
246 def get_dst_type(cls):
247 return cls.nebula_type__dst_type__
251 return cls.nebula_type__labels__
254 def get_multiedge_keys(cls):
255 return cls.nebula_type__multiedge_key__
259 return edge_model_to_type(cls)
261 return EdgeModelDynamic
269 name: Optional[str] =
None,
271 if isinstance(type_desc, GraphType):
273 raise ValueError(
"name is required for GraphType")
274 type_desc = type_desc.nodes[name]
275 elif isinstance(type_desc, NodeType):
278 raise ValueError(f
"Invalid type description: {type_desc}")
282 type_desc.pr_or_me_keys,
292 name: Optional[str] =
None,
294 if isinstance(type_desc, GraphType):
296 raise ValueError(
"name is required for GraphType")
297 type_desc = type_desc.edges[name]
298 elif isinstance(type_desc, EdgeType):
301 raise ValueError(f
"Invalid type description: {type_desc}")
303 type_desc.src_node_type,
305 type_desc.dst_node_type,
307 type_desc.pr_or_me_keys,
311def _node_expr(node: NodeModel) -> str:
312 return f
"(@`{node.get_type()}` {{ {unwrap_props(node.get_properties())} }})"
315def _edge_expr(edge: EdgeModel) -> str:
316 return f
"[@`{edge.get_type()}` {{ {unwrap_props(edge.get_properties())} }}]"
319def upsert_batch_nodes_gql(
320 items: Sequence[NodeModel],
321 batch_size: int = 1024,
323 for i
in range(0, len(items), batch_size):
324 batch = items[i : i + batch_size]
325 batch_str =
", ".join(_node_expr(node)
for node
in batch)
326 yield f
"INSERT OR UPDATE {batch_str}"
331 edge: Optional[EdgeModel] =
None,
332 dst_node: Optional[NodeModel] =
None,
334 """Upsert a node or a triplet (node, edge, dst_node), and return the GQL string"""
336 return f
"""INSERT OR UPDATE {_node_expr(node)}"""
339 raise ValueError(
"dst_node is required when edge is provided")
340 return f
"""INSERT OR UPDATE {_node_expr(node)}-{_edge_expr(edge)}->{_node_expr(dst_node)}"""
343def upsert_edge_only_gql(
344 src: Union[int, Dict[str, Any], NodeModel],
346 dst: Union[int, Dict[str, Any], NodeModel],
348 """Take in the element_id or the properties filter for the source and destination nodes,
349 and return the GQL string for the edge upsert"""
352 idt: str, type_: str, element: Union[int, Dict[str, Any], NodeModel]
354 base = f
"({idt}@`{type_}`"
355 if isinstance(element, int):
356 return f
"{base} WHERE element_id({idt}) = {element} )"
357 elif isinstance(element, NodeModel):
358 return f
"{base} {{ {unwrap_props(element.get_primary_kv())} }} )"
360 return f
"{base} {{ {unwrap_props(element)} }} )"
362 src_pattern = _get_pattern(
"u", edge.get_src_type(), src)
363 dst_pattern = _get_pattern(
"v", edge.get_dst_type(), dst)
364 return f
"""MATCH {src_pattern}, {dst_pattern} INSERT OR UPDATE (u)-{_edge_expr(edge)}->(v)"""
367def _check_optional(t: Any) -> tuple[bool, type]:
369 Check if a type annotation is Optional[T], Union[T, None], or T | None.
370 Returns a tuple of (is_optional, inner_type).
373 t: A type annotation, typically from a pydantic model_field.annotation
376 A tuple of (is_optional, inner_type) where:
377 - is_optional: True if the type is optional, False otherwise
378 - inner_type: The underlying type T (without the Optional wrapper)
382 raise ValueError(
"t is None")
385 origin = get_origin(t)
389 if origin
is Union
or origin
is UnionType:
391 if (arg0 := args[0])
is type(
None)
or (arg1 := args[1])
is type(
None):
392 return True, arg0
if arg0
is not type(
None)
else arg1
394 raise ValueError(
"Union type should have exactly one None type")
396 raise ValueError(
"Union type should have exactly two types")
409DATA_TYPE_KEYS = [*DATA_TYPE_MAPPING.keys(), NVector]
412@dataclass(frozen=True)
414 """Annotation for that in BasicTargetType but not primitive type"""
416 name: Literal[
"AsJSON",
"AsNVector"]
421 def __class_getitem__(cls, T: type):
423 raise ValueError(
"AsJSON does not support str as the inner type")
428 lambda v: (TypeAdapter(T).validate_json(v)
if isinstance(v, str)
else v)
430 PlainSerializer(
lambda v: TypeAdapter(T).dump_json(v).decode(
"utf-8")),
436 """Adapter utilities for Nebula vectors with serialization helpers."""
439 def load(value: Sequence[float] | NVector) -> NVector:
440 """Coerce plain sequences into NVectors to simplify model loading."""
442 if isinstance(value, NVector):
444 if isinstance(value, (str, bytes)):
445 raise TypeError(
"NVector expects a numeric sequence, got string-like input")
446 if isinstance(value, ABCSequence):
447 return NVector(values=[float(component)
for component
in value])
448 raise TypeError(f
"Cannot coerce value of type {type(value)!r} into NVector")
451 def dump(value: NVector) -> list[float]:
452 """Convert an NVector into a plain list for persistence/transport."""
454 return list(value.get_values())
457 def save(value: Sequence[float] | NVector) -> list[float]:
458 """Coerce the provided value and dump it in one call for convenience."""
460 return AsNVector.dump(AsNVector.load(value))
462 def __class_getitem__(cls, size: int):
463 def after_validator(v: NVector) -> NVector:
464 if v.dimension != size:
465 raise AssertionError(
466 f
"Vector dimension mismatch: got {v.dimension} dim but the type should be dim {size}"
472 BeforeValidator(AsNVector.load),
473 AfterValidator(after_validator),
474 PlainSerializer(AsNVector.dump),
479def _scan_metadata(t: type, field_info: FieldInfo) -> Optional[AdditionalMetadata]:
480 for meta
in [*getattr(t,
"__metadata__", []), *field_info.metadata]:
481 if isinstance(meta, AdditionalMetadata):
486def get_data_type(t: type, field_info: FieldInfo) -> str:
487 if t
in DATA_TYPE_MAPPING:
488 return DATA_TYPE_MAPPING[t]
491 if get_origin(t)
is list:
492 inner_type = get_args(t)[0]
494 inner_type_name = DATA_TYPE_MAPPING[inner_type]
495 except KeyError
as e:
497 f
"Unsupported inner type: {inner_type} for List"
499 return f
"LIST<{inner_type_name}>"
502 meta = _scan_metadata(t, field_info)
504 if meta.name ==
"AsNVector":
505 return f
"VECTOR<{meta.data}, FLOAT>"
506 elif meta.name ==
"AsJSON":
509 raise NotImplementedError(f
"Unsupported metadata: {meta.name}")
512 f
"No metadata found for the field {field_info}, which is not a primitive type."
516def get_prop(obj: NodeModel | EdgeModel, key: str) -> tuple[str, BasicTargetType]:
517 field = obj.__class__.model_fields[key]
519 def _get_alias() -> str:
525 _, t = _check_optional(field.annotation)
526 meta = _scan_metadata(t, field)
528 if meta
is not None and meta.name ==
"AsNVector":
529 v = getattr(obj, key)
530 return (_get_alias(), v)
538 return (_get_alias(), v)
541def get_props(obj: NodeModel | EdgeModel) -> Dict[str, BasicTargetType]:
543 get_prop(obj, k)
for k, v
in obj.__class__.model_fields.items()
if not v.exclude
547def _encode_properties(props: Mapping[str, FieldInfo]):
548 res: Dict[str, PropTypeRow] = {}
549 for k, v
in props.items():
550 if k.startswith(
"nebula_obj__"):
553 is_optional, inner_type = _check_optional(v.annotation)
556 data_type=get_data_type(inner_type, v),
557 nullable=is_optional,
564def node_model_to_type(node: Type[NodeModel]) -> NodeType:
565 pk = node.get_primary_keys()
566 props = _encode_properties(node.model_fields)
568 node_type=node.get_type(),
569 labels=list(node.get_labels()),
575def edge_model_to_type(edge: Type[EdgeModel]) -> EdgeType:
576 mek = edge.get_multiedge_keys()
577 props = _encode_properties(edge.model_fields)
579 edge_type=edge.get_type(),
580 src_node_type=edge.get_src_type(),
581 dst_node_type=edge.get_dst_type(),
582 labels=list(edge.get_labels()),
list[float] save(Sequence[float]|NVector value)
NVector load(Sequence[float]|NVector value)
list[float] dump(NVector value)
tuple[str, BasicTargetType] get_prop(self, str key)
List[str] get_multiedge_keys(cls)
Optional[int] get_rank(self)
List[str] get_primary_keys(cls)
tuple[str, BasicTargetType] get_prop(self, str key)