Graphinate 0.9.0__tar.gz → 0.10.0__tar.gz

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.
Files changed (129) hide show
  1. {graphinate-0.9.0 → graphinate-0.10.0}/.github/workflows/ci.yml +1 -1
  2. {graphinate-0.9.0 → graphinate-0.10.0}/PKG-INFO +3 -3
  3. {graphinate-0.9.0 → graphinate-0.10.0}/examples/code/python_ast.py +1 -1
  4. {graphinate-0.9.0 → graphinate-0.10.0}/examples/system/files.py +8 -4
  5. {graphinate-0.9.0 → graphinate-0.10.0}/examples/system/processes.py +7 -1
  6. graphinate-0.10.0/playground/social/requirements.txt +1 -0
  7. {graphinate-0.9.0 → graphinate-0.10.0}/pyproject.toml +7 -3
  8. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/builders.py +44 -5
  9. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/cli.py +17 -5
  10. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/color.py +2 -1
  11. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/renderers/graphql.py +2 -2
  12. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/starlette/views.py +4 -2
  13. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/viewer/index.html +29 -27
  14. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/test_builders.py +3 -2
  15. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/test_server.py +1 -1
  16. {graphinate-0.9.0 → graphinate-0.10.0}/.coveragerc +0 -0
  17. {graphinate-0.9.0 → graphinate-0.10.0}/.deepsource.toml +0 -0
  18. {graphinate-0.9.0 → graphinate-0.10.0}/.github/dependabot.yml +0 -0
  19. {graphinate-0.9.0 → graphinate-0.10.0}/.github/workflows/codeql.yml +0 -0
  20. {graphinate-0.9.0 → graphinate-0.10.0}/.github/workflows/publish-docs.yaml +0 -0
  21. {graphinate-0.9.0 → graphinate-0.10.0}/.github/workflows/publish.yml +0 -0
  22. {graphinate-0.9.0 → graphinate-0.10.0}/.github/workflows/test-beta.yml +0 -0
  23. {graphinate-0.9.0 → graphinate-0.10.0}/.github/workflows/test.yml +0 -0
  24. {graphinate-0.9.0 → graphinate-0.10.0}/.gitignore +0 -0
  25. {graphinate-0.9.0 → graphinate-0.10.0}/.sonarcloud.properties +0 -0
  26. {graphinate-0.9.0 → graphinate-0.10.0}/LICENSE +0 -0
  27. {graphinate-0.9.0 → graphinate-0.10.0}/README.md +0 -0
  28. {graphinate-0.9.0 → graphinate-0.10.0}/docs/.dates_cache.json +0 -0
  29. {graphinate-0.9.0 → graphinate-0.10.0}/docs/acknowledgements.md +0 -0
  30. {graphinate-0.9.0 → graphinate-0.10.0}/docs/assets/badge/v0.json +0 -0
  31. {graphinate-0.9.0 → graphinate-0.10.0}/docs/assets/images/logo-128.png +0 -0
  32. {graphinate-0.9.0 → graphinate-0.10.0}/docs/assets/images/network_graph.png +0 -0
  33. {graphinate-0.9.0 → graphinate-0.10.0}/docs/assets/stylesheets/extra.css +0 -0
  34. {graphinate-0.9.0 → graphinate-0.10.0}/docs/examples/code.md +0 -0
  35. {graphinate-0.9.0 → graphinate-0.10.0}/docs/examples/github.md +0 -0
  36. {graphinate-0.9.0 → graphinate-0.10.0}/docs/examples/math.md +0 -0
  37. {graphinate-0.9.0 → graphinate-0.10.0}/docs/examples/social.md +0 -0
  38. {graphinate-0.9.0 → graphinate-0.10.0}/docs/examples/system.md +0 -0
  39. {graphinate-0.9.0 → graphinate-0.10.0}/docs/examples/web.md +0 -0
  40. {graphinate-0.9.0 → graphinate-0.10.0}/docs/gen_ref_pages.py +0 -0
  41. {graphinate-0.9.0 → graphinate-0.10.0}/docs/index.md +0 -0
  42. {graphinate-0.9.0 → graphinate-0.10.0}/docs/introduction.md +0 -0
  43. {graphinate-0.9.0 → graphinate-0.10.0}/docs/start.md +0 -0
  44. {graphinate-0.9.0 → graphinate-0.10.0}/docs/tutorial.md +0 -0
  45. {graphinate-0.9.0 → graphinate-0.10.0}/docs/usage/cli.md +0 -0
  46. {graphinate-0.9.0 → graphinate-0.10.0}/docs/usage/lib.md +0 -0
  47. {graphinate-0.9.0 → graphinate-0.10.0}/examples/code/git_commits.py +0 -0
  48. {graphinate-0.9.0 → graphinate-0.10.0}/examples/code/python_dependencies.py +0 -0
  49. {graphinate-0.9.0 → graphinate-0.10.0}/examples/code/requirements.txt +0 -0
  50. {graphinate-0.9.0 → graphinate-0.10.0}/examples/code/tokens.py +0 -0
  51. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/_client.py +0 -0
  52. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/commits_visibilty_graph.py +0 -0
  53. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/followers.graphql +0 -0
  54. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/followers.py +0 -0
  55. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/graphql.config.yml +0 -0
  56. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/repositories.graphql +0 -0
  57. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/repositories.py +0 -0
  58. {graphinate-0.9.0 → graphinate-0.10.0}/examples/github/requirements.txt +0 -0
  59. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/__init__.py +0 -0
  60. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/_test_materializers.py +0 -0
  61. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/graph_atlas.py +0 -0
  62. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/graphs.py +0 -0
  63. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/gui.py +0 -0
  64. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/materializers.py +0 -0
  65. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/polygonal_graph.py +0 -0
  66. {graphinate-0.9.0 → graphinate-0.10.0}/examples/math/requirements.txt +0 -0
  67. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/13/dd/73ce25face7beb30b69b64feeb77.val +0 -0
  68. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/21/9e/00846f323987ba16cfbe0127d8eb.val +0 -0
  69. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/70/b6/2aefb0269adce7fedf877fa0d267.val +0 -0
  70. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/87/f5/ec1739bc369e84c3fcb302bf532a.val +0 -0
  71. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/ba/fe/3aca7b2c38abff60e7ce5eb486a8.val +0 -0
  72. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/c7/9e/ce82b0288020b7152779df09bd73.val +0 -0
  73. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/cache.db +0 -0
  74. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/d2/53/3b88f2fc162561cfdbbe9abc352a.val +0 -0
  75. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/cache/e2/d5/5d079f200eabf9b625b0473f6fbe.val +0 -0
  76. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/gui.py +0 -0
  77. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/music_artists.py +0 -0
  78. {graphinate-0.9.0 → graphinate-0.10.0}/examples/social/requirements.txt +0 -0
  79. {graphinate-0.9.0 → graphinate-0.10.0}/examples/system/.ignore +0 -0
  80. {graphinate-0.9.0 → graphinate-0.10.0}/examples/system/requirements.txt +0 -0
  81. {graphinate-0.9.0 → graphinate-0.10.0}/examples/web/html_dom.py +0 -0
  82. {graphinate-0.9.0 → graphinate-0.10.0}/examples/web/page_links.py +0 -0
  83. {graphinate-0.9.0 → graphinate-0.10.0}/examples/web/requirements.txt +0 -0
  84. {graphinate-0.9.0 → graphinate-0.10.0}/mkdocs.yml +0 -0
  85. {graphinate-0.9.0 → graphinate-0.10.0}/playground/ethernet/traceroute.py +0 -0
  86. {graphinate-0.9.0 → graphinate-0.10.0}/playground/genric_graph.graphql +0 -0
  87. {graphinate-0.9.0 → graphinate-0.10.0}/playground/graphql.config.yml +0 -0
  88. {graphinate-0.9.0 → graphinate-0.10.0}/playground/house_of_graphs.py +0 -0
  89. {graphinate-0.9.0 → graphinate-0.10.0}/playground/science/caffeine.py +0 -0
  90. {graphinate-0.9.0 → graphinate-0.10.0}/playground/social/albums.json +0 -0
  91. {graphinate-0.9.0 → graphinate-0.10.0}/playground/social/musicisians.py +0 -0
  92. {graphinate-0.9.0 → graphinate-0.10.0}/playground/text/nlp_graph.py +0 -0
  93. {graphinate-0.9.0 → graphinate-0.10.0}/playground/text/requirements.txt +0 -0
  94. {graphinate-0.9.0 → graphinate-0.10.0}/playground/time_series/requirements.txt +0 -0
  95. {graphinate-0.9.0 → graphinate-0.10.0}/playground/time_series/visibility_graph.py +0 -0
  96. {graphinate-0.9.0 → graphinate-0.10.0}/sonar-project.properties +0 -0
  97. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/__init__.py +0 -0
  98. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/__main__.py +0 -0
  99. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/constants.py +0 -0
  100. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/converters.py +0 -0
  101. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/modeling.py +0 -0
  102. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/renderers/__init__.py +0 -0
  103. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/renderers/matplotlib.py +0 -0
  104. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/__init__.py +0 -0
  105. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/starlette/__init__.py +0 -0
  106. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/__init__.py +0 -0
  107. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/elements/__init__.py +0 -0
  108. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/elements/index.html +0 -0
  109. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/graphiql/__init__.py +0 -0
  110. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/graphiql/index.html +0 -0
  111. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/rapidoc/__init__.py +0 -0
  112. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/rapidoc/index.html +0 -0
  113. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/static/images/logo-128.png +0 -0
  114. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/static/images/logo.svg +0 -0
  115. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/static/images/network_graph.png +0 -0
  116. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/viewer/__init__.py +0 -0
  117. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/voyager/__init__.py +0 -0
  118. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/server/web/voyager/index.html +0 -0
  119. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/tools.py +0 -0
  120. {graphinate-0.9.0 → graphinate-0.10.0}/src/graphinate/typing.py +0 -0
  121. {graphinate-0.9.0 → graphinate-0.10.0}/tests/conftest.py +0 -0
  122. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/renderers/test_graphql.py +0 -0
  123. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/renderers/test_matplotlib_draw.py +0 -0
  124. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/renderers/test_matplotlib_plot.py +0 -0
  125. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/server/test_starlette.py +0 -0
  126. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/test_cli.py +0 -0
  127. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/test_color.py +0 -0
  128. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/test_converters.py +0 -0
  129. {graphinate-0.9.0 → graphinate-0.10.0}/tests/graphinate/test_modeling.py +0 -0
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  python-version: ${{ matrix.python-version }}
27
27
  - name: Install dependencies
28
- run: uv sync
28
+ run: uv sync --only-group dev
29
29
  - name: Run Ruff Linter
30
30
  run: uvx ruff check .
31
31
  - name: Check Ruff Formatting
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Graphinate
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Graphinate. Data to Graphs.
5
5
  Project-URL: Homepage, https://erivlis.github.io/graphinate
6
6
  Project-URL: Documentation, https://erivlis.github.io/graphinate
@@ -34,8 +34,8 @@ Requires-Dist: loguru
34
34
  Requires-Dist: mappingtools
35
35
  Requires-Dist: matplotlib
36
36
  Requires-Dist: networkx
37
- Requires-Dist: networkx-mermaid>=0.3.0
38
- Requires-Dist: networkx-query>=2.1.3
37
+ Requires-Dist: networkx-mermaid
38
+ Requires-Dist: networkx-query
39
39
  Requires-Dist: strawberry-graphql[asgi,opentelemetry]
40
40
  Provides-Extra: plot
41
41
  Requires-Dist: scipy; extra == 'plot'
@@ -55,7 +55,7 @@ def ast_graph_model():
55
55
 
56
56
  graph_model = graphinate.model(name=f'AST Graph - {code_object.__qualname__}',)
57
57
 
58
- root_ast_node = ast.parse(inspect.getsource(graphinate.builders.D3Builder))
58
+ root_ast_node = ast.parse(inspect.getsource(code_object))
59
59
 
60
60
  def node_type(ast_node):
61
61
  return ast_node.__class__.__name__
@@ -1,13 +1,14 @@
1
1
  import fnmatch
2
2
  import operator
3
3
  import pathlib
4
+ from collections.abc import Iterable
4
5
 
5
6
  from magika import Magika
6
7
 
7
8
  import graphinate
8
9
 
9
10
 
10
- def load_ignore_patterns(ignore_files):
11
+ def load_ignore_patterns(ignore_files: Iterable[str]) -> set[str]:
11
12
  patterns = set()
12
13
  for ignore_file in ignore_files:
13
14
  if pathlib.Path(ignore_file).exists():
@@ -19,17 +20,20 @@ def load_ignore_patterns(ignore_files):
19
20
  return patterns
20
21
 
21
22
 
22
- def is_ignored(path, patterns):
23
+ def is_ignored(path: pathlib.Path, patterns: Iterable[str]) -> bool:
23
24
  return any(fnmatch.fnmatch(path.as_posix(), pattern) for pattern in patterns)
24
25
 
25
26
 
26
- def create_filesystem_graph_model(input_folder='.', ignore_files=['.ignore', '.gitignore', '.dockerignore']):
27
+ def create_filesystem_graph_model(
28
+ input_folder: str = '.',
29
+ ignore_files: Iterable[str] = ('.ignore', '.gitignore', '.dockerignore')
30
+ ):
27
31
  """
28
32
  Create a graph model of the file system structure.
29
33
 
30
34
  Args:
31
35
  input_folder (str): The folder to start the traversal from. Defaults to the current folder.
32
- ignore_files (list): A list of files containing ignore patterns.
36
+ ignore_files (Iterable): A list of files containing ignore patterns.
33
37
  Defaults to ['.ignore', '.gitignore', '.dockerignore'].
34
38
 
35
39
  Returns:
@@ -1,3 +1,9 @@
1
+ """
2
+ Example: Processes Graph
3
+ This example demonstrates how to create a graph model representing
4
+ processes and their parent-child relationships using the `psutil` library.
5
+ """
6
+
1
7
  import operator
2
8
  from collections.abc import Iterable
3
9
 
@@ -61,4 +67,4 @@ if __name__ == '__main__':
61
67
 
62
68
  # Or
63
69
  # 3. Option B - Output as a plot
64
- graphinate.materializers.plot(graph)
70
+ graphinate.renderers.matplotlib.plot(graph)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Graphinate"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "Graphinate. Data to Graphs."
5
5
  authors = [
6
6
  { name = "Eran Rivlis", email = "eran@rivlis.info" },
@@ -35,8 +35,8 @@ dependencies = [
35
35
  "mappingtools",
36
36
  "matplotlib",
37
37
  "networkx",
38
- "networkx-mermaid>=0.3.0",
39
- "networkx-query>=2.1.3",
38
+ "networkx-mermaid",
39
+ "networkx-query",
40
40
  "strawberry-graphql[asgi,opentelemetry]",
41
41
  ]
42
42
 
@@ -56,6 +56,10 @@ server = [
56
56
  "Source" = "https://github.com/erivlis/graphinate"
57
57
 
58
58
 
59
+ [project.scripts]
60
+ graphinate = "graphinate.cli:cli"
61
+
62
+
59
63
  [dependency-groups]
60
64
  dev = [
61
65
  "ruff",
@@ -438,6 +438,10 @@ class GraphQLBuilder(NetworkxBuilder):
438
438
  created: datetime | None
439
439
  updated: datetime | None
440
440
 
441
+ @strawberry.enum
442
+ class GraphNodeType(Enum):
443
+ ... # pragma: no cover
444
+
441
445
  @strawberry.interface(description="Represents a Graph Node")
442
446
  class GraphNode(GraphElement):
443
447
  node_id: strawberry.ID
@@ -445,7 +449,9 @@ class GraphQLBuilder(NetworkxBuilder):
445
449
  lineage: str
446
450
 
447
451
  @strawberry.field()
448
- def neighbors(self, children: bool = False) -> list[Optional['GraphQLBuilder.GraphNode']]:
452
+ def neighbors(self,
453
+ type: 'GraphQLBuilder.GraphNodeType | None' = None,
454
+ children: bool = False) -> list[Optional['GraphQLBuilder.GraphNode']]:
449
455
  ... # pragma: no cover
450
456
 
451
457
  @strawberry.field()
@@ -628,30 +634,63 @@ class GraphQLBuilder(NetworkxBuilder):
628
634
  description=f"Represents a {capitalized_name} Graph Node"
629
635
  )
630
636
 
637
+ @staticmethod
638
+ def _graphql_enum(name: str, values: list[str]) -> type[Enum]:
639
+ return strawberry.enum(
640
+ Enum(name, {v: v for v in values}),
641
+ name=name,
642
+ description=f"{name} Enumeration"
643
+ )
644
+
631
645
  @classmethod
632
646
  @functools.lru_cache
633
647
  def _children_types(cls, model: GraphModel, node_type: str):
634
648
  return model.node_children_types(node_type).get(node_type, [])
635
649
 
650
+ def _populate_graph_node_type_enum(self, node_types: list[str]):
651
+ from strawberry.types.enum import EnumValue
652
+
653
+ for v in node_types:
654
+ self.GraphNodeType._member_names_.append(v)
655
+ self.GraphNodeType._member_map_[v] = v
656
+ self.GraphNodeType._value2member_map_[v] = v
657
+
658
+ self.GraphNodeType._enum_definition.values.append(
659
+ EnumValue(
660
+ name=v,
661
+ value=v,
662
+ description=f"Graph Node Type: {v}"
663
+ )
664
+ )
665
+
636
666
  @property
637
667
  @functools.lru_cache
638
668
  def _graphql_types(self) -> dict[str, type['GraphQLBuilder.GraphNode']]:
639
669
  node_types = list(self._graph.graph['node_types'].keys())
640
670
 
671
+ self._populate_graph_node_type_enum(node_types)
672
+
641
673
  def neighbors_resolver():
642
674
  graph = self._graph
643
675
 
644
676
  children_types = set(self._children_types(self.model, node_type))
645
677
 
646
- def node_neighbors(self, children: bool = False) -> list[GraphQLBuilder.GraphNode | None]:
678
+ def node_neighbors(self,
679
+ type: 'GraphQLBuilder.GraphNodeType | None' = None,
680
+ children: bool = False) -> list['GraphQLBuilder.GraphNode']:
647
681
  node = decode_id(self.id)
648
- items = (GraphQLBuilder._graph_node(graphql_types[d['type']], n, d) for n, d in graph.nodes(data=True)
682
+ items = (GraphQLBuilder._graph_node(graphql_types[d['type']], n, d)
683
+ for n, d in graph.nodes(data=True)
649
684
  if n in graph.neighbors(node))
650
685
 
686
+ if type is not None:
687
+ items = (item for item in items if item.type == type)
688
+
651
689
  if children and children_types:
652
- return [item for item in items if item.type in children_types]
690
+ items = (item for item in items if item.type in children_types)
653
691
 
654
- return list(items)
692
+ items = list(items)
693
+ return items
655
694
 
656
695
  return node_neighbors
657
696
 
@@ -1,5 +1,6 @@
1
1
  import importlib
2
2
  import json
3
+ from pathlib import Path
3
4
  from typing import Any
4
5
 
5
6
  import click
@@ -9,7 +10,7 @@ from graphinate.renderers.graphql import DEFAULT_PORT
9
10
 
10
11
 
11
12
  def _get_kwargs(ctx) -> dict:
12
- return dict([item.strip('--').split('=') for item in ctx.args if item.startswith("--")])
13
+ return dict([item.strip('--').split('=') for item in ctx.args if item.startswith("--")]) # NOSONAR
13
14
 
14
15
 
15
16
  def import_from_string(import_str: Any) -> Any:
@@ -72,15 +73,26 @@ model_option = click.option('-m', '--model',
72
73
  @click.group()
73
74
  @click.pass_context
74
75
  def cli(ctx):
75
- pass
76
+ ctx.ensure_object(dict)
76
77
 
77
78
 
78
79
  @cli.command()
79
80
  @model_option
80
81
  @click.pass_context
81
- def save(ctx, model):
82
+ def save(ctx, model: GraphModel):
83
+ file_path = Path(f"{model.name}.d3_graph.json")
84
+
85
+ if file_path.is_absolute():
86
+ raise click.ClickException("Please provide a relative file path for saving the graph.")
87
+
88
+ if file_path.parent != Path('.'):
89
+ raise click.ClickException("Saving to subdirectories is not supported. Please provide a file name only.")
90
+
91
+ if file_path.exists():
92
+ click.confirm(f"The file '{file_path}' already exists. Do you want to overwrite it?", abort=True)
93
+
82
94
  kwargs = _get_kwargs(ctx)
83
- with open(f"{model.name}.d3_graph.json", mode='w') as fp:
95
+ with open(file_path, mode='w') as fp:
84
96
  graph = builders.D3Builder(model, **kwargs).build()
85
97
  json.dump(graph, fp=fp, default=str, **kwargs)
86
98
 
@@ -100,4 +112,4 @@ def server(ctx, model: GraphModel, port: int, browse: bool):
100
112
  ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚══════╝"""
101
113
  click.echo(message)
102
114
  schema = builders.GraphQLBuilder(model).build()
103
- graphql(schema, port=port, browse=browse, **_get_kwargs(ctx))
115
+ graphql.server(schema, port=port, browse=browse, **_get_kwargs(ctx))
@@ -61,7 +61,8 @@ def color_hex(color: Union[str, Sequence[Union[float, int]]]) -> Union[str, Sequ
61
61
  msg = "Input values should either be a float between 0 and 1 or an int between 0 and 255"
62
62
  raise ValueError(msg)
63
63
 
64
- return '#{:02x}{:02x}{:02x}'.format(*rgb)
64
+ r, g, b = rgb
65
+ return f'#{r:02x}{g:02x}{b:02x}'
65
66
 
66
67
  else:
67
68
  return color
@@ -28,7 +28,7 @@ def _openapi_schema(request: Request) -> ASGIApp:
28
28
  """
29
29
  schema_data = {
30
30
  'openapi': '3.0.0',
31
- 'info': {'title': 'Graphinate API', 'version': '0.9.0'},
31
+ 'info': {'title': 'Graphinate API', 'version': '0.10.0'},
32
32
  'paths': {
33
33
  '/graphql': {'get': {'responses': {200: {'description': 'GraphQL'}}}},
34
34
  '/graphiql': {'get': {'responses': {200: {'description': 'GraphiQL UI.'}}}},
@@ -80,7 +80,7 @@ def _starlette_app(graphql_app: strawberry.asgi.GraphQL | None = None, port: int
80
80
  app.add_route("/schema", route=_openapi_schema, include_in_schema=False)
81
81
  app.add_route("/openapi.json", route=_openapi_schema, include_in_schema=False)
82
82
 
83
- async def redirect_to_viewer(request):
83
+ def redirect_to_viewer(request):
84
84
  return RedirectResponse(url='/viewer')
85
85
 
86
86
  app.add_route('/', redirect_to_viewer)
@@ -1,13 +1,15 @@
1
+ import functools
2
+
1
3
  from starlette.responses import FileResponse
2
4
  from starlette.routing import Route
3
5
 
4
6
  from ..web import get_static_path
5
7
 
6
8
 
7
- async def favicon(request):
9
+ def favicon(request):
8
10
  path = get_static_path('images/logo-128.png').absolute().as_posix()
9
11
  return FileResponse(path)
10
12
 
11
-
13
+ @functools.cache
12
14
  def favicon_route() -> Route:
13
15
  return Route('/favicon.ico', endpoint=favicon, include_in_schema=False)
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="">
2
+ <html lang="en">
3
3
  <head>
4
4
  <title>Graphinate Viewer</title>
5
5
  <link rel="modulepreload"
@@ -16,8 +16,8 @@
16
16
  import * as TweakpaneEssentialsPlugin
17
17
  from 'https://cdn.jsdelivr.net/npm/@tweakpane/plugin-essentials@0.2.1/dist/tweakpane-plugin-essentials.min.js';
18
18
  // Export it as a global variable
19
- window.Tweakpane = Tweakpane;
20
- window.TweakpaneEssentialsPlugin = TweakpaneEssentialsPlugin;
19
+ globalThis.Tweakpane = Tweakpane;
20
+ globalThis.TweakpaneEssentialsPlugin = TweakpaneEssentialsPlugin;
21
21
  </script>
22
22
  <script src="https://cdn.jsdelivr.net/npm/3d-force-graph@1.76.2/dist/3d-force-graph.min.js"
23
23
  integrity="sha384-zSKZ615fogcuRjUjE+0YstAo3N/t7PzUvtQu5KGA1iqkmHXsTRst7zP+6573D+hK"
@@ -96,12 +96,12 @@
96
96
 
97
97
  function lowerZIndexForHigherPanels(panel) {
98
98
  const panels = document.querySelectorAll('.floating-panel');
99
- panels.forEach(p => {
100
- const zIndex = parseInt(window.getComputedStyle(p).zIndex, 10);
101
- if (zIndex > parseInt(panel.style.zIndex, 10)) {
99
+ for (const p of panels) {
100
+ const zIndex = Number.parseInt(globalThis.getComputedStyle(p).zIndex, 10);
101
+ if (zIndex > Number.parseInt(panel.style.zIndex, 10)) {
102
102
  p.style.zIndex = zIndex - 1;
103
103
  }
104
- });
104
+ }
105
105
  }
106
106
 
107
107
  // Function to bring the panel to the front
@@ -170,7 +170,7 @@
170
170
  closeButton.addEventListener('click', () => {
171
171
  lowerZIndexForHigherPanels(panel);
172
172
  highestZIndex--;
173
- document.body.removeChild(panel);
173
+ panel.remove();
174
174
  });
175
175
 
176
176
  // Create the maximize toggle button
@@ -335,7 +335,7 @@
335
335
  .linkDirectionalArrowLength(link => graphParams.linkDirectionalArrowLength)
336
336
  .linkDirectionalArrowRelPos(link => graphParams.linkDirectionalArrowRelPos)
337
337
  .linkDirectionalParticles(link => graphParams.linkDirectionalParticles)
338
- .linkDirectionalParticleSpeed(link => graphParams.linkDirectionalParticleSpeed / 10000.0)
338
+ .linkDirectionalParticleSpeed(link => graphParams.linkDirectionalParticleSpeed / 10000)
339
339
  .linkDirectionalParticleWidth(link => graphParams.linkDirectionalParticleWidth)
340
340
  .linkDirectionalParticleColor(link => graphParams.linkDirectionalParticleColor)
341
341
  .cooldownTicks(100);
@@ -377,9 +377,9 @@
377
377
  const linePos = line.geometry.getAttribute('position');
378
378
 
379
379
  // calculate coordinate on the node's surface instead of center
380
- linePos.set([startR / lineLen, 1 - endR / lineLen].map(t =>
380
+ linePos.set([startR / lineLen, 1 - endR / lineLen].flatMap(t =>
381
381
  ['x', 'y', 'z'].map(dim => start[dim] + (end[dim] - start[dim]) * t)
382
- ).flat());
382
+ ));
383
383
  linePos.needsUpdate = true;
384
384
  return true;
385
385
  });
@@ -392,33 +392,34 @@
392
392
 
393
393
  function getVisible(gData) {
394
394
  const visibleNodes = []
395
- gData.nodes.forEach((node) => {
395
+ for (const node of gData.nodes) {
396
396
  if (node.type in nodeTypeVisibility && nodeTypeVisibility[node.type]) {
397
397
  visibleNodes.push(node);
398
398
  }
399
- });
399
+ }
400
+
400
401
  const visibleLinks = [];
401
- gData.links.forEach((link) => {
402
- if (visibleNodes.find((node) => node.id === link.source.id) && visibleNodes.find((node) => node.id === link.target.id)) {
402
+ for (const link of gData.links) {
403
+ if (visibleNodes.find((node) => node.id === link.source.id) && visibleNodes.find((node) => node.id === link.target.id)) {
403
404
  visibleLinks.push(link);
404
405
  }
405
- });
406
+ }
406
407
 
407
408
  return {nodes: visibleNodes, links: visibleLinks};
408
409
  }
409
410
 
410
411
  function setAllNodeTypeVisibility(value) {
411
- Object.keys(nodeTypeVisibility).forEach(key => {
412
+ for (const key of Object.keys(nodeTypeVisibility)) {
412
413
  nodeTypeVisibility[key] = value;
413
- });
414
+ }
414
415
  }
415
416
 
416
417
  function updateNodeTypeColorMapping(nodes) {
417
- nodes.forEach(node => {
418
+ for (const node of nodes) {
418
419
  if (!nodeTypeColor[node.type]) {
419
420
  nodeTypeColor[node.type] = node.color;
420
421
  }
421
- });
422
+ }
422
423
  }
423
424
 
424
425
  function refreshGraph(gData) {
@@ -451,17 +452,19 @@
451
452
  label: 'Visibility',
452
453
  }).on('click', (ev) => {
453
454
  switch (ev.cell.title) {
454
- case 'All On':
455
+ case 'All On': {
455
456
  setAllNodeTypeVisibility(true);
456
457
  updateGraph(gData);
457
458
  pane.refresh();
458
459
  break;
459
- case 'All Off':
460
+ }
461
+ case 'All Off': {
460
462
  setAllNodeTypeVisibility(false);
461
463
  const visibleGData = getVisible(gData);
462
464
  updateGraph(visibleGData);
463
465
  pane.refresh();
464
466
  break;
467
+ }
465
468
  default:
466
469
  console.log('Unknown action');
467
470
  }
@@ -491,7 +494,7 @@
491
494
  // Legend tab
492
495
  const legendTab = tab.pages[0];
493
496
 
494
- graph.nodeTypes.forEach(nodeType => {
497
+ for (const nodeType of graph.nodeTypes) {
495
498
  let info = `V: ${nodeType.count}`
496
499
  const edgeType = graph.edgeTypes.find((t) => t.name === nodeType.name);
497
500
  if (edgeType) {
@@ -502,11 +505,11 @@
502
505
 
503
506
  nodeTypeFolder.addBinding(nodeTypeColor, nodeType.name, {label: 'Color'})
504
507
  .on('change', (ev) => {
505
- gData.nodes.forEach(node => {
508
+ for (const node of gData.nodes) {
506
509
  if (node.type === nodeType.name) {
507
510
  node.color = ev.value;
508
511
  }
509
- });
512
+ }
510
513
  refreshGraph(gData);
511
514
  });
512
515
 
@@ -515,8 +518,7 @@
515
518
  .on('change', (ev) => {
516
519
  refreshGraph(gData);
517
520
  });
518
- });
519
-
521
+ }
520
522
 
521
523
  // Advanced tab
522
524
 
@@ -490,8 +490,9 @@ def test_populate_nodes_empty_generator(builder_with_graph):
490
490
  node_type_absolute_id = ('parent', 'child')
491
491
 
492
492
  def generator(**kwargs):
493
- return
494
- yield
493
+ """Dummy generator that yields nothing"""
494
+ return # NOSONAR
495
+ yield # NOSONAR
495
496
 
496
497
  node_model = DummyNodeModel(generator=generator)
497
498
  builder_with_graph.model._node_models = {node_type_absolute_id: [node_model]}
@@ -13,7 +13,7 @@ def test_get_static_path():
13
13
 
14
14
  @pytest.mark.asyncio
15
15
  async def test_favicon():
16
- actual = await graphinate.server.starlette.views.favicon(None)
16
+ actual = graphinate.server.starlette.views.favicon(None)
17
17
 
18
18
  assert isinstance(actual, FileResponse)
19
19
  assert actual.media_type == 'image/png'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes