1818from datetime import datetime , date , time , timedelta
1919from enum import Enum
2020import json
21- from typing import Any , Dict , Iterable , List , Optional , Set , Tuple , Union
21+ from typing import Any , ClassVar , Dict , Iterable , List , Optional , Set , Tuple , Union
2222
23- from pydantic . v1 import BaseModel , Extra , Field , PrivateAttr # noqa F401
23+ from pydantic import BaseModel , ConfigDict , Field as PydanticField , PrivateAttr # noqa F401
2424
2525from gqlalchemy .exceptions import (
2626 GQLAlchemyError ,
@@ -48,6 +48,64 @@ class DatetimeKeywords(Enum):
4848}
4949
5050
51+ def _get_model_fields (model : Any ) -> Dict [str , Any ]:
52+ model_fields = getattr (model , "model_fields" , None )
53+ if model_fields is not None :
54+ return model_fields
55+ return getattr (model , "__fields__" , {})
56+
57+
58+ def _get_field_attrs (field : Any ) -> Dict [str , Any ]:
59+ attrs = getattr (field , "json_schema_extra" , None )
60+ if isinstance (attrs , dict ):
61+ return attrs
62+
63+ field_info = getattr (field , "field_info" , None )
64+ if field_info is not None :
65+ extra = getattr (field_info , "extra" , None )
66+ if isinstance (extra , dict ):
67+ return extra
68+ json_schema_extra = getattr (field_info , "json_schema_extra" , None )
69+ if isinstance (json_schema_extra , dict ):
70+ return json_schema_extra
71+
72+ return {}
73+
74+
75+ def _set_field_attrs (field : Any , attrs : Dict [str , Any ]) -> None :
76+ copied_attrs = dict (attrs )
77+ if hasattr (field , "json_schema_extra" ):
78+ field .json_schema_extra = copied_attrs
79+ return
80+
81+ field_info = getattr (field , "field_info" , None )
82+ if field_info is not None and hasattr (field_info , "extra" ):
83+ field_info .extra = copied_attrs
84+
85+
86+ def _get_field_type_name (field : Any ) -> str :
87+ if hasattr (field , "type_" ):
88+ return field .type_ .__name__
89+
90+ annotation = getattr (field , "annotation" , None )
91+ return getattr (annotation , "__name__" , str (annotation ))
92+
93+
94+ def Field (default = ..., ** kwargs ): # noqa N802
95+ """Pydantic Field wrapper that stores custom OGM metadata in json_schema_extra."""
96+ json_schema_extra = kwargs .pop ("json_schema_extra" , None )
97+ extras = dict (json_schema_extra or {})
98+
99+ for attr in ("index" , "exists" , "unique" , "db" , "on_disk" , "label" ):
100+ if attr in kwargs :
101+ extras [attr ] = kwargs .pop (attr )
102+
103+ if extras :
104+ kwargs ["json_schema_extra" ] = extras
105+
106+ return PydanticField (default , ** kwargs )
107+
108+
51109def _format_timedelta (duration : timedelta ) -> str :
52110 days = int (duration .total_seconds () // 86400 )
53111 remainder_sec = duration .total_seconds () - days * 86400
@@ -352,10 +410,16 @@ def to_cypher(self) -> str:
352410
353411
354412class GraphObject (BaseModel ):
355- _subtypes_ : Dict = dict ()
413+ _subtypes_ : ClassVar [Dict [str , Any ]] = {}
414+ model_config = ConfigDict (extra = "allow" , coerce_numbers_to_str = True )
356415
357- class Config :
358- extra = Extra .allow
416+ def __init__ (self , ** data ):
417+ # Preserve explicitly provided underscore-prefixed attributes.
418+ private_data = {key : value for key , value in data .items () if key .startswith ("_" )}
419+ super ().__init__ (** data )
420+ for key , value in private_data .items ():
421+ if key in self .__private_attributes__ :
422+ setattr (self , key , value )
359423
360424 def __init_subclass__ (cls , type = None , label = None , labels = None , index = None , db = None ):
361425 """Stores the subclass by type if type is specified, or by class name
@@ -401,12 +465,21 @@ def _convert_to_real_type_(cls, data):
401465
402466 return sub (** data )
403467
468+ @classmethod
469+ def model_validate (cls , obj , * args , ** kwargs ):
470+ """Used to convert a dictionary object into the appropriate GraphObject."""
471+ if isinstance (obj , cls ):
472+ return obj
473+ if isinstance (obj , dict ):
474+ return cls ._convert_to_real_type_ (obj )
475+ return super ().model_validate (obj , * args , ** kwargs )
476+
404477 @classmethod
405478 def parse_obj (cls , obj ):
406479 """Used to convert a dictionary object into the appropriate
407480 GraphObject.
408481 """
409- return cls ._convert_to_real_type_ (obj )
482+ return cls .model_validate (obj )
410483
411484 def escape_value (
412485 self , value : Union [None , bool , int , float , str , list , dict , datetime , timedelta , date , time ]
@@ -463,7 +536,7 @@ def _get_cypher_field_assignment_block(self, variable_name: str, operator: str)
463536 "user.name = 'John' AND user.age = 34"
464537 """
465538 cypher_fields = []
466- for field in self . __fields__ :
539+ for field in _get_model_fields ( type ( self )) :
467540 value = getattr (self , field )
468541 if value is not None :
469542 cypher_fields .append (f"{ variable_name } .{ field } = { self .escape_value (value )} " )
@@ -493,8 +566,8 @@ def _get_cypher_fields_xor_block(self, variable_name: str) -> str:
493566 def _get_cypher_set_properties (self , variable_name : str ) -> str :
494567 """Returns a cypher set properties block."""
495568 cypher_set_properties = []
496- for field in self . __fields__ :
497- attributes = self . __fields__ [ field ]. field_info . extra
569+ for field , field_definition in _get_model_fields ( type ( self )). items () :
570+ attributes = _get_field_attrs ( field_definition )
498571 value = getattr (self , field )
499572 if value is not None and not attributes .get ("on_disk" , False ):
500573 cypher_set_properties .append (f" SET { variable_name } .{ field } = { self .escape_value (value )} " )
@@ -520,7 +593,7 @@ def __init__(self, **data):
520593
521594 @property
522595 def _properties (self ) -> Dict [str , Any ]: # noqa: F811
523- return {k : v for k , v in dict ( self ).items () if not k .startswith ("_" ) and k != "labels" }
596+ return {k : v for k , v in self . model_dump ( ).items () if not k .startswith ("_" ) and k != "labels" }
524597
525598 def __str__ (self ) -> str :
526599 return f"<GraphObject id={ self ._id } properties={ self ._properties } >"
@@ -539,8 +612,9 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
539612 def field_in_superclass (field , constraint ):
540613 nonlocal bases
541614 for base in bases :
542- if field in base .__fields__ :
543- attrs = base .__fields__ [field ].field_info .extra
615+ base_fields = _get_model_fields (base )
616+ if field in base_fields :
617+ attrs = _get_field_attrs (base_fields [field ])
544618 if constraint in attrs :
545619 return base
546620
@@ -569,9 +643,10 @@ def get_base_labels() -> Set[str]:
569643 index = MemgraphIndex (cls .label )
570644 db .create_index (index )
571645
572- for field in cls .__fields__ :
573- attrs = cls .__fields__ [field ].field_info .extra
574- field_type = cls .__fields__ [field ].type_ .__name__
646+ cls_fields = _get_model_fields (cls )
647+ for field , field_definition in cls_fields .items ():
648+ attrs = _get_field_attrs (field_definition )
649+ field_type = _get_field_type_name (field_definition )
575650 label = attrs .get ("label" , cls .label )
576651 skip_constraints = False
577652
@@ -582,7 +657,8 @@ def get_base_labels() -> Set[str]:
582657 if constraint in attrs and db is None :
583658 base = field_in_superclass (field , constraint )
584659 if base is not None :
585- cls .__fields__ [field ].field_info .extra = base .__fields__ [field ].field_info .extra
660+ base_fields = _get_model_fields (base )
661+ _set_field_attrs (field_definition , _get_field_attrs (base_fields [field ]))
586662 skip_constraints = True
587663 break
588664
@@ -634,8 +710,8 @@ def __str__(self) -> str:
634710 def _get_cypher_unique_fields_or_block (self , variable_name : str ) -> str :
635711 """Get's a cypher assignment block using the unique fields."""
636712 cypher_unique_fields = []
637- for field in self . __fields__ :
638- attrs = self . __fields__ [ field ]. field_info . extra
713+ for field , field_definition in _get_model_fields ( type ( self )). items () :
714+ attrs = _get_field_attrs ( field_definition )
639715 if "unique" in attrs :
640716 value = getattr (self , field )
641717 if value is not None :
@@ -645,8 +721,8 @@ def _get_cypher_unique_fields_or_block(self, variable_name: str) -> str:
645721
646722 def has_unique_fields (self ) -> bool :
647723 """Returns True if the Node has any unique fields."""
648- for field in self . __fields__ :
649- if "unique" in self . __fields__ [ field ]. field_info . extra :
724+ for field , field_definition in _get_model_fields ( type ( self )). items () :
725+ if "unique" in _get_field_attrs ( field_definition ) :
650726 if getattr (self , field ) is not None :
651727 return True
652728 return False
@@ -665,7 +741,7 @@ def save(self, db: "Database") -> "Node": # noqa F821
665741 Null properties are ignored.
666742 """
667743 node = db .save_node (self )
668- for field in self . __fields__ :
744+ for field in _get_model_fields ( type ( self )) :
669745 setattr (self , field , getattr (node , field ))
670746 self ._id = node ._id
671747 return self
@@ -681,7 +757,7 @@ def load(self, db: "Database") -> "Node": # noqa F821
681757 If no node is found or no properties are set it raises a GQLAlchemyError.
682758 """
683759 node = db .load_node (self )
684- for field in self . __fields__ :
760+ for field in _get_model_fields ( type ( self )) :
685761 setattr (self , field , getattr (node , field ))
686762 self ._id = node ._id
687763 return self
@@ -754,7 +830,7 @@ def save(self, db: "Database") -> "Relationship": # noqa F821
754830 relationship, use `load_relationship` first.
755831 """
756832 relationship = db .save_relationship (self )
757- for field in self . __fields__ :
833+ for field in _get_model_fields ( type ( self )) :
758834 setattr (self , field , getattr (relationship , field ))
759835 self ._id = relationship ._id
760836 return self
@@ -770,7 +846,7 @@ def load(self, db: "Database") -> "Relationship": # noqa F821
770846 multiple relationships like that in Memgraph, throws GQLAlchemyError.
771847 """
772848 relationship = db .load_relationship (self )
773- for field in self . __fields__ :
849+ for field in _get_model_fields ( type ( self )) :
774850 setattr (self , field , getattr (relationship , field ))
775851 self ._id = relationship ._id
776852 return self
0 commit comments