neo4j-viz 0.3.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
neo4j_viz/gql_create.py CHANGED
@@ -249,13 +249,8 @@ def from_gql_create(
249
249
  node_pattern = re.compile(r"^\(([^)]*)\)$")
250
250
  rel_pattern = re.compile(r"^\(([^)]*)\)-\s*\[\s*:(\w+)\s*(\{[^}]*\})?\s*\]->\(([^)]*)\)$")
251
251
 
252
- node_top_level_keys = set(Node.model_fields.keys())
253
- node_top_level_keys.remove("id")
254
-
255
- rel_top_level_keys = set(Relationship.model_fields.keys())
256
- rel_top_level_keys.remove("id")
257
- rel_top_level_keys.remove("source")
258
- rel_top_level_keys.remove("target")
252
+ node_top_level_keys = Node.all_validation_aliases(exempted_fields=["id"])
253
+ rel_top_level_keys = Relationship.all_validation_aliases(exempted_fields=["id", "source", "target"])
259
254
 
260
255
  nodes = []
261
256
  relationships = []
neo4j_viz/neo4j.py CHANGED
@@ -47,10 +47,15 @@ def from_neo4j(
47
47
  else:
48
48
  raise ValueError(f"Invalid input type `{type(result)}`. Expected `neo4j.Graph` or `neo4j.Result`")
49
49
 
50
- nodes = [_map_node(node, size_property, caption_property=node_caption) for node in graph.nodes]
50
+ all_node_field_aliases = Node.all_validation_aliases()
51
+ all_rel_field_aliases = Relationship.all_validation_aliases()
52
+
53
+ nodes = [
54
+ _map_node(node, all_node_field_aliases, size_property, caption_property=node_caption) for node in graph.nodes
55
+ ]
51
56
  relationships = []
52
57
  for rel in graph.relationships:
53
- mapped_rel = _map_relationship(rel, caption_property=relationship_caption)
58
+ mapped_rel = _map_relationship(rel, all_rel_field_aliases, caption_property=relationship_caption)
54
59
  if mapped_rel:
55
60
  relationships.append(mapped_rel)
56
61
 
@@ -62,7 +67,12 @@ def from_neo4j(
62
67
  return VG
63
68
 
64
69
 
65
- def _map_node(node: neo4j.graph.Node, size_property: Optional[str], caption_property: Optional[str]) -> Node:
70
+ def _map_node(
71
+ node: neo4j.graph.Node,
72
+ all_node_field_aliases: set[str],
73
+ size_property: Optional[str],
74
+ caption_property: Optional[str],
75
+ ) -> Node:
66
76
  top_level_fields = {"id": node.element_id}
67
77
 
68
78
  if size_property:
@@ -78,7 +88,7 @@ def _map_node(node: neo4j.graph.Node, size_property: Optional[str], caption_prop
78
88
 
79
89
  properties = {}
80
90
  for prop, value in node.items():
81
- if prop not in Node.model_fields.keys():
91
+ if prop not in all_node_field_aliases:
82
92
  properties[prop] = value
83
93
  continue
84
94
 
@@ -95,7 +105,9 @@ def _map_node(node: neo4j.graph.Node, size_property: Optional[str], caption_prop
95
105
  return Node(**top_level_fields, properties=properties)
96
106
 
97
107
 
98
- def _map_relationship(rel: neo4j.graph.Relationship, caption_property: Optional[str]) -> Optional[Relationship]:
108
+ def _map_relationship(
109
+ rel: neo4j.graph.Relationship, all_rel_field_aliases: set[str], caption_property: Optional[str]
110
+ ) -> Optional[Relationship]:
99
111
  if rel.start_node is None or rel.end_node is None:
100
112
  return None
101
113
 
@@ -109,7 +121,7 @@ def _map_relationship(rel: neo4j.graph.Relationship, caption_property: Optional[
109
121
 
110
122
  properties = {}
111
123
  for prop, value in rel.items():
112
- if prop not in Relationship.model_fields.keys():
124
+ if prop not in all_rel_field_aliases:
113
125
  properties[prop] = value
114
126
  continue
115
127
 
neo4j_viz/node.py CHANGED
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, Optional, Union
4
4
 
5
- from pydantic import AliasChoices, BaseModel, Field, field_serializer, field_validator
5
+ from pydantic import AliasChoices, AliasGenerator, BaseModel, Field, field_serializer, field_validator
6
+ from pydantic.alias_generators import to_camel
6
7
  from pydantic_extra_types.color import Color, ColorType
7
8
 
8
9
  from .node_size import RealNumber
@@ -11,29 +12,46 @@ from .options import CaptionAlignment
11
12
  NodeIdType = Union[str, int]
12
13
 
13
14
 
14
- class Node(BaseModel, extra="allow"):
15
+ def create_aliases(field_name: str) -> AliasChoices:
16
+ valid_names = [field_name]
17
+
18
+ if field_name == "id":
19
+ valid_names.extend(["nodeid", "node_id"])
20
+
21
+ choices = [[choice, choice.upper(), to_camel(choice)] for choice in valid_names]
22
+
23
+ return AliasChoices(*[alias for aliases in choices for alias in aliases])
24
+
25
+
26
+ class Node(
27
+ BaseModel,
28
+ extra="forbid",
29
+ alias_generator=AliasGenerator(
30
+ validation_alias=create_aliases,
31
+ serialization_alias=lambda field_name: to_camel(field_name),
32
+ ),
33
+ ):
15
34
  """
16
35
  A node in a graph to visualize.
17
36
 
37
+ Each field is case-insensitive for input, and camelCase is also accepted.
38
+ For example, "CAPTION_ALIGN", "captionAlign" are also valid inputs keys for the `caption_align` field.
39
+ Upon construction however, the field names are converted to snake_case.
40
+
18
41
  For more info on each field, see the NVL library docs: https://neo4j.com/docs/nvl/current/base-library/#_nodes
19
42
  """
20
43
 
21
44
  #: Unique identifier for the node
22
- id: NodeIdType = Field(
23
- validation_alias=AliasChoices("id", "nodeId", "node_id"), description="Unique identifier for the node"
24
- )
45
+ id: NodeIdType = Field(description="Unique identifier for the node")
25
46
  #: The caption of the node
26
47
  caption: Optional[str] = Field(None, description="The caption of the node")
27
48
  #: The alignment of the caption text
28
- caption_align: Optional[CaptionAlignment] = Field(
29
- None, serialization_alias="captionAlign", description="The alignment of the caption text"
30
- )
49
+ caption_align: Optional[CaptionAlignment] = Field(None, description="The alignment of the caption text")
31
50
  #: The size of the caption text. The font size to node radius ratio
32
51
  caption_size: Optional[int] = Field(
33
52
  None,
34
53
  ge=1,
35
54
  le=3,
36
- serialization_alias="captionSize",
37
55
  description="The size of the caption text. The font size to node radius ratio",
38
56
  )
39
57
  #: The size of the node as radius in pixel
@@ -70,3 +88,12 @@ class Node(BaseModel, extra="allow"):
70
88
 
71
89
  def to_dict(self) -> dict[str, Any]:
72
90
  return self.model_dump(exclude_none=True, by_alias=True)
91
+
92
+ @staticmethod
93
+ def all_validation_aliases(exempted_fields: Optional[list[str]] = None) -> set[str]:
94
+ if exempted_fields is None:
95
+ exempted_fields = []
96
+
97
+ by_field = [v.validation_alias.choices for k, v in Node.model_fields.items() if k not in exempted_fields] # type: ignore
98
+
99
+ return {str(alias) for aliases in by_field for alias in aliases}
neo4j_viz/nvl.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import uuid
5
5
  from importlib.resources import files
6
+ from typing import Union
6
7
 
7
8
  from IPython.display import HTML
8
9
 
@@ -40,10 +41,28 @@ class NVL:
40
41
  with screenshot_path.open("r", encoding="utf-8") as file:
41
42
  self.screenshot_svg = file.read()
42
43
 
43
- def unsupported_field_type_error(self, e: TypeError, entity: str) -> Exception:
44
- if "not JSON serializable" in str(e):
45
- return ValueError(f"A field of a {entity} object is not supported: {str(e)}")
46
- return e
44
+ @staticmethod
45
+ def _serialize_entity(entity: Union[Node, Relationship]) -> str:
46
+ try:
47
+ entity_dict = entity.to_dict()
48
+ return json.dumps(entity_dict)
49
+ except TypeError:
50
+ props_as_strings = {}
51
+ for k, v in entity_dict["properties"].items():
52
+ try:
53
+ json.dumps(v)
54
+ except TypeError:
55
+ props_as_strings[k] = str(v)
56
+ entity_dict["properties"].update(props_as_strings)
57
+
58
+ try:
59
+ return json.dumps(entity_dict)
60
+ except TypeError as e:
61
+ # This should never happen anymore, but just in case
62
+ if "not JSON serializable" in str(e):
63
+ raise ValueError(f"A field of a {type(entity).__name__} object is not supported: {str(e)}")
64
+ else:
65
+ raise e
47
66
 
48
67
  def render(
49
68
  self,
@@ -54,14 +73,8 @@ class NVL:
54
73
  height: str,
55
74
  show_hover_tooltip: bool,
56
75
  ) -> HTML:
57
- try:
58
- nodes_json = json.dumps([node.to_dict() for node in nodes])
59
- except TypeError as e:
60
- raise self.unsupported_field_type_error(e, "node")
61
- try:
62
- rels_json = json.dumps([rel.to_dict() for rel in relationships])
63
- except TypeError as e:
64
- raise self.unsupported_field_type_error(e, "relationship")
76
+ nodes_json = f"[{','.join([self._serialize_entity(node) for node in nodes])}]"
77
+ rels_json = f"[{','.join([self._serialize_entity(rel) for rel in relationships])}]"
65
78
 
66
79
  render_options_json = json.dumps(render_options.to_dict())
67
80
  container_id = str(uuid.uuid4())
@@ -112,7 +125,7 @@ class NVL:
112
125
  <script>
113
126
  getTheme = () => {{
114
127
  const backgroundColorString = window.getComputedStyle(document.body, null).getPropertyValue('background-color')
115
- const colorsArray = backgroundColorString.match(/\d+/g);
128
+ const colorsArray = backgroundColorString.match(/\\d+/g);
116
129
  const brightness = Number(colorsArray[0]) * 0.2126 + Number(colorsArray[1]) * 0.7152 + Number(colorsArray[2]) * 0.0722
117
130
  return brightness < 128 ? "dark" : "light"
118
131
  }}
neo4j_viz/pandas.py CHANGED
@@ -23,6 +23,9 @@ def _from_dfs(
23
23
  else:
24
24
  node_dfs_iter = node_dfs
25
25
 
26
+ all_node_field_aliases = Node.all_validation_aliases()
27
+ all_rel_field_aliases = Relationship.all_validation_aliases()
28
+
26
29
  has_size = True
27
30
  nodes = []
28
31
  for node_df in node_dfs_iter:
@@ -31,7 +34,7 @@ def _from_dfs(
31
34
  top_level = {}
32
35
  properties = {}
33
36
  for key, value in row.to_dict().items():
34
- if key in Node.model_fields.keys():
37
+ if key in all_node_field_aliases:
35
38
  top_level[key] = value
36
39
  else:
37
40
  if rename_properties and key in rename_properties:
@@ -51,7 +54,7 @@ def _from_dfs(
51
54
  top_level = {}
52
55
  properties = {}
53
56
  for key, value in row.to_dict().items():
54
- if key in Relationship.model_fields.keys():
57
+ if key in all_rel_field_aliases:
55
58
  top_level[key] = value
56
59
  else:
57
60
  if rename_properties and key in rename_properties:
neo4j_viz/relationship.py CHANGED
@@ -3,16 +3,41 @@ from __future__ import annotations
3
3
  from typing import Any, Optional, Union
4
4
  from uuid import uuid4
5
5
 
6
- from pydantic import AliasChoices, BaseModel, Field, field_serializer, field_validator
6
+ from pydantic import AliasChoices, AliasGenerator, BaseModel, Field, field_serializer, field_validator
7
+ from pydantic.alias_generators import to_camel
7
8
  from pydantic_extra_types.color import Color, ColorType
8
9
 
9
10
  from .options import CaptionAlignment
10
11
 
11
12
 
12
- class Relationship(BaseModel, extra="allow"):
13
+ def create_aliases(field_name: str) -> AliasChoices:
14
+ valid_names = [field_name]
15
+
16
+ if field_name == "source":
17
+ valid_names.extend(["sourcenodeid", "source_node_id", "from"])
18
+ if field_name == "target":
19
+ valid_names.extend(["targetnodeid", "target_node_id", "to"])
20
+
21
+ choices = [[choice, choice.upper(), to_camel(choice)] for choice in valid_names]
22
+
23
+ return AliasChoices(*[alias for aliases in choices for alias in aliases])
24
+
25
+
26
+ class Relationship(
27
+ BaseModel,
28
+ extra="forbid",
29
+ alias_generator=AliasGenerator(
30
+ validation_alias=create_aliases,
31
+ serialization_alias=lambda field_name: to_camel(field_name),
32
+ ),
33
+ ):
13
34
  """
14
35
  A relationship in a graph to visualize.
15
36
 
37
+ Each field is case-insensitive for input, and camelCase is also accepted.
38
+ For example, "CAPTION_ALIGN", "captionAlign" are also valid inputs keys for the `caption_align` field.
39
+ Upon construction however, the field names are converted to snake_case.
40
+
16
41
  For more info on each field, see the NVL library docs: https://neo4j.com/docs/nvl/current/base-library/#_relationships
17
42
  """
18
43
 
@@ -23,25 +48,19 @@ class Relationship(BaseModel, extra="allow"):
23
48
  #: Node ID where the relationship points from
24
49
  source: Union[str, int] = Field(
25
50
  serialization_alias="from",
26
- validation_alias=AliasChoices("source", "sourceNodeId", "source_node_id", "from"),
27
51
  description="Node ID where the relationship points from",
28
52
  )
29
53
  #: Node ID where the relationship points to
30
54
  target: Union[str, int] = Field(
31
55
  serialization_alias="to",
32
- validation_alias=AliasChoices("target", "targetNodeId", "target_node_id", "to"),
33
56
  description="Node ID where the relationship points to",
34
57
  )
35
58
  #: The caption of the relationship
36
59
  caption: Optional[str] = Field(None, description="The caption of the relationship")
37
60
  #: The alignment of the caption text
38
- caption_align: Optional[CaptionAlignment] = Field(
39
- None, serialization_alias="captionAlign", description="The alignment of the caption text"
40
- )
61
+ caption_align: Optional[CaptionAlignment] = Field(None, description="The alignment of the caption text")
41
62
  #: The size of the caption text
42
- caption_size: Optional[Union[int, float]] = Field(
43
- None, gt=0.0, serialization_alias="captionSize", description="The size of the caption text"
44
- )
63
+ caption_size: Optional[Union[int, float]] = Field(None, gt=0.0, description="The size of the caption text")
45
64
  #: The color of the relationship. Allowed input is for example "#FF0000", "red" or (255, 0, 0)
46
65
  color: Optional[ColorType] = Field(None, description="The color of the relationship")
47
66
  #: Additional properties of the relationship that do not directly impact the visualization
@@ -76,3 +95,16 @@ class Relationship(BaseModel, extra="allow"):
76
95
 
77
96
  def to_dict(self) -> dict[str, Any]:
78
97
  return self.model_dump(exclude_none=True, by_alias=True)
98
+
99
+ @staticmethod
100
+ def all_validation_aliases(exempted_fields: Optional[list[str]] = None) -> set[str]:
101
+ if exempted_fields is None:
102
+ exempted_fields = []
103
+
104
+ by_field = [
105
+ v.validation_alias.choices # type: ignore
106
+ for k, v in Relationship.model_fields.items()
107
+ if k not in exempted_fields
108
+ ]
109
+
110
+ return {str(alias) for aliases in by_field for alias in aliases}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neo4j-viz
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A simple graph visualization tool
5
5
  Author-email: Neo4j <team-gds@neo4j.org>
6
6
  Project-URL: Homepage, https://neo4j.com/
@@ -1,22 +1,22 @@
1
1
  neo4j_viz/__init__.py,sha256=ATDPdaupFWIWaOWtJtfCGVRVuuwXKNe1hNdENofgRLk,270
2
2
  neo4j_viz/colors.py,sha256=IvOCTmCu7WTMna_wNLZ3GrThTwFyIoKtNkmZYDLdYac,6694
3
3
  neo4j_viz/gds.py,sha256=v6b3XdSoWSoOu-U2MWQM-5QXLiPT3_9gGOv7haxuozE,4135
4
- neo4j_viz/gql_create.py,sha256=Eqo_fFxtsrsoKV0gjXIpHZdeBqjX2q-vwe_xBCJU7OI,13354
5
- neo4j_viz/neo4j.py,sha256=0urKoYwtCyyKXMu_tWPspehyjOFF5ETyiWuYVNA1Wfc,4589
6
- neo4j_viz/node.py,sha256=NgB8preP2v1PWW177iU4YeojvfWafz8yIyY7MQrP6iE,2850
4
+ neo4j_viz/gql_create.py,sha256=YH9mZUx-b86h714wCIbikGoxZD7IEuOmhKDdrp5fhXk,13264
5
+ neo4j_viz/neo4j.py,sha256=UTrLZRlDgkw2w8Ov4P1oP8w-25Kf0sMCkLFDq3qD-Q8,4855
6
+ neo4j_viz/node.py,sha256=MiLoghsn2NLs_iV65NuW7u3iaxP8MTKoNy6La9TdreY,3886
7
7
  neo4j_viz/node_size.py,sha256=c_sMtQSD8eJ_6Y0Kr6ku0LOs9VoEDxfYCUUzUWZ-1Xo,1197
8
- neo4j_viz/nvl.py,sha256=32NTuNCaf78bHEVD1nWQD5jUHS2ZUBemxKktuIpiCK0,4774
8
+ neo4j_viz/nvl.py,sha256=ZN3tyWar9ugR88r5N6txW3ThfNEWOt5A1KzrrRnLKwk,5262
9
9
  neo4j_viz/options.py,sha256=QBeHLqHOJFtSCjzHvv-2A9TzTbnIaC6_onQq9fUlT_4,3079
10
- neo4j_viz/pandas.py,sha256=4ArBUV6DKOhwbplD8edJd5DsIilDNKyCpPjqrus0bPk,3269
10
+ neo4j_viz/pandas.py,sha256=bju4Pcoe-d_MFG6XOpVPPkD_p3cF-gWuXqfA9oxyvSU,3382
11
11
  neo4j_viz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- neo4j_viz/relationship.py,sha256=DmmBUHup_jtqnenQKACCfz4Mz7CjbCnDmOZfsSj6qiQ,3042
12
+ neo4j_viz/relationship.py,sha256=sRgGjNzlqt6wPmiB3WXBTxR_W_5Z40VofpHUZvkRS1I,4143
13
13
  neo4j_viz/visualization_graph.py,sha256=ogYhxAyPzqOcSogYSesGAV6iRQDLIA-YhBImEShuCPM,13175
14
14
  neo4j_viz/resources/icons/screenshot.svg,sha256=Ns9Yi2Iq4lIaiFvzc0pXBmjxt4fcmBO-I4cI8Xiu1HE,311
15
15
  neo4j_viz/resources/icons/zoom-in.svg,sha256=PsO5yFkA1JnGM2QV_qxHKG13qmoR-RrlWARpaXNp5qU,415
16
16
  neo4j_viz/resources/icons/zoom-out.svg,sha256=OQRADAoe2bxbCeFufg6W22nR41q5NlI8QspT9l5pXUw,400
17
17
  neo4j_viz/resources/nvl_entrypoint/base.js,sha256=QfFjqd7Uo2F-plgFlPfLzUcjqzTzqUh533VaBemJhLI,1816247
18
18
  neo4j_viz/resources/nvl_entrypoint/styles.css,sha256=JjeTSB9OJT2KMfb8yFUUMLMG7Rzrf3o60hSCD547zTk,1123
19
- neo4j_viz-0.3.0.dist-info/METADATA,sha256=wCM57O61k4kllIiYOqopDJiQPBzyftooJwhwz9h8oSw,7074
20
- neo4j_viz-0.3.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
21
- neo4j_viz-0.3.0.dist-info/top_level.txt,sha256=jPUM3z8MOtxqDanc2VzqkxG4HJn8aaq4S7rnCFNk_Vs,10
22
- neo4j_viz-0.3.0.dist-info/RECORD,,
19
+ neo4j_viz-0.3.1.dist-info/METADATA,sha256=p8n2aK9ELDWZX75mUyDCh1pLR22TdWrQarXTXmXYcAU,7074
20
+ neo4j_viz-0.3.1.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
21
+ neo4j_viz-0.3.1.dist-info/top_level.txt,sha256=jPUM3z8MOtxqDanc2VzqkxG4HJn8aaq4S7rnCFNk_Vs,10
22
+ neo4j_viz-0.3.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5