Skip to content

Commit 12a0bcb

Browse files
committed
Use v2 of pydantic API
1 parent 98f1181 commit 12a0bcb

8 files changed

Lines changed: 133 additions & 56 deletions

File tree

gqlalchemy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import warnings
1616

17-
from pydantic.v1 import validator # noqa F401
17+
from pydantic import validator # noqa F401
1818

1919
from gqlalchemy.models import ( # noqa F401
2020
MemgraphConstraintExists,

gqlalchemy/connection.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def _create_connection(self) -> Connection:
119119
def _convert_memgraph_value(value: Any) -> Any:
120120
"""Converts Memgraph objects to custom Node/Relationship objects."""
121121
if isinstance(value, mgclient.Relationship):
122-
return Relationship.parse_obj(
122+
return Relationship.model_validate(
123123
{
124124
"_type": value.type,
125125
"_id": value.id,
@@ -130,7 +130,7 @@ def _convert_memgraph_value(value: Any) -> Any:
130130
)
131131

132132
if isinstance(value, mgclient.Node):
133-
return Node.parse_obj(
133+
return Node.model_validate(
134134
{
135135
"_id": value.id,
136136
"_labels": set(value.labels),
@@ -139,7 +139,7 @@ def _convert_memgraph_value(value: Any) -> Any:
139139
)
140140

141141
if isinstance(value, mgclient.Path):
142-
return Path.parse_obj(
142+
return Path.model_validate(
143143
{
144144
"_nodes": list([_convert_memgraph_value(node) for node in value.nodes]),
145145
"_relationships": list([_convert_memgraph_value(rel) for rel in value.relationships]),
@@ -192,7 +192,7 @@ def _create_connection(self):
192192
def _convert_neo4j_value(value: Any) -> Any:
193193
"""Converts Neo4j objects to custom Node/Relationship objects."""
194194
if isinstance(value, Neo4jRelationship):
195-
return Relationship.parse_obj(
195+
return Relationship.model_validate(
196196
{
197197
"_type": value.type,
198198
"_id": value.id,
@@ -203,7 +203,7 @@ def _convert_neo4j_value(value: Any) -> Any:
203203
)
204204

205205
if isinstance(value, Neo4jNode):
206-
return Node.parse_obj(
206+
return Node.model_validate(
207207
{
208208
"_id": value.id,
209209
"_labels": set(value.labels),
@@ -212,7 +212,7 @@ def _convert_neo4j_value(value: Any) -> Any:
212212
)
213213

214214
if isinstance(value, Neo4jPath):
215-
return Path.parse_obj(
215+
return Path.model_validate(
216216
{
217217
"_nodes": list([_convert_neo4j_value(node) for node in value.nodes]),
218218
"_relationships": list([_convert_neo4j_value(rel) for rel in value.relationships]),

gqlalchemy/models.py

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from datetime import datetime, date, time, timedelta
1919
from enum import Enum
2020
import 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

2525
from 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+
51109
def _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

354412
class 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

gqlalchemy/vendors/memgraph.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
MemgraphTrigger,
3636
Node,
3737
Relationship,
38+
_get_field_attrs,
39+
_get_model_fields,
3840
)
3941
from gqlalchemy.vendors.database_client import DatabaseClient
4042
from gqlalchemy.graph_algorithms.query_modules import QueryModule
@@ -346,9 +348,9 @@ def _save_node_properties_on_disk(self, node: Node, result: Node) -> Node:
346348
"""Saves all on_disk properties to the on disk database attached to
347349
the database.
348350
"""
349-
for field in node.__fields__:
351+
for field, field_definition in _get_model_fields(type(node)).items():
350352
value = getattr(node, field, None)
351-
if value is not None and "on_disk" in node.__fields__[field].field_info.extra:
353+
if value is not None and "on_disk" in _get_field_attrs(field_definition):
352354
if self.on_disk_db is None:
353355
raise GQLAlchemyOnDiskPropertyDatabaseNotDefinedError()
354356
self.on_disk_db.save_node_property(result._id, field, value)
@@ -381,9 +383,9 @@ def load_node(self, node: Node) -> Optional[Node]:
381383

382384
def _load_node_properties_on_disk(self, result: Node) -> Node:
383385
"""Loads all on_disk properties from the on disk database."""
384-
for field in result.__fields__:
386+
for field, field_definition in _get_model_fields(type(result)).items():
385387
value = getattr(result, field, None)
386-
if "on_disk" in result.__fields__[field].field_info.extra:
388+
if "on_disk" in _get_field_attrs(field_definition):
387389
if self.on_disk_db is None:
388390
raise GQLAlchemyOnDiskPropertyDatabaseNotDefinedError()
389391
try:
@@ -420,9 +422,9 @@ def _load_relationship_properties_on_disk(self, result: Relationship) -> Relatio
420422
Memgraph().init_disk_storage() throws a
421423
GQLAlchemyOnDiskPropertyDatabaseNotDefinedError.
422424
"""
423-
for field in result.__fields__:
425+
for field, field_definition in _get_model_fields(type(result)).items():
424426
value = getattr(result, field, None)
425-
if "on_disk" in result.__fields__[field].field_info.extra:
427+
if "on_disk" in _get_field_attrs(field_definition):
426428
if self.on_disk_db is None:
427429
raise GQLAlchemyOnDiskPropertyDatabaseNotDefinedError()
428430
try:
@@ -456,9 +458,9 @@ def _save_relationship_properties_on_disk(self, relationship: Relationship, resu
456458
added with Memgraph().init_disk_storage(db). If OnDiskPropertyDatabase
457459
is not defined raises GQLAlchemyOnDiskPropertyDatabaseNotDefinedError.
458460
"""
459-
for field in relationship.__fields__:
461+
for field, field_definition in _get_model_fields(type(relationship)).items():
460462
value = getattr(relationship, field, None)
461-
if value is not None and "on_disk" in relationship.__fields__[field].field_info.extra:
463+
if value is not None and "on_disk" in _get_field_attrs(field_definition):
462464
if self.on_disk_db is None:
463465
raise GQLAlchemyOnDiskPropertyDatabaseNotDefinedError()
464466
self.on_disk_db.save_relationship_property(result._id, field, value)

0 commit comments

Comments
 (0)