Graphinate 0.4.0__tar.gz → 0.5.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 (103) hide show
  1. {graphinate-0.4.0 → graphinate-0.5.0}/PKG-INFO +9 -5
  2. {graphinate-0.4.0 → graphinate-0.5.0}/README.md +7 -3
  3. graphinate-0.5.0/examples/math/graph_atlas.py +69 -0
  4. {graphinate-0.4.0 → graphinate-0.5.0}/examples/math/graphs.py +74 -20
  5. graphinate-0.5.0/examples/math/gui.py +108 -0
  6. {graphinate-0.4.0 → graphinate-0.5.0}/examples/web/page_links.py +12 -8
  7. {graphinate-0.4.0 → graphinate-0.5.0}/pyproject.toml +2 -2
  8. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/builders.py +5 -5
  9. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/modeling.py +14 -15
  10. graphinate-0.5.0/src/graphinate/server/web/static/images/logo.svg +50 -0
  11. graphinate-0.5.0/src/graphinate/server/web/viewer/index.html +162 -0
  12. graphinate-0.4.0/examples/math/graph_atlas.py +0 -52
  13. graphinate-0.4.0/examples/math/gui.py +0 -133
  14. graphinate-0.4.0/src/graphinate/server/web/static/js/murmurhash3_gc.js +0 -64
  15. graphinate-0.4.0/src/graphinate/server/web/viewer/index.html +0 -66
  16. {graphinate-0.4.0 → graphinate-0.5.0}/.coveragerc +0 -0
  17. {graphinate-0.4.0 → graphinate-0.5.0}/.github/dependabot.yml +0 -0
  18. {graphinate-0.4.0 → graphinate-0.5.0}/.github/workflows/codeql.yml +0 -0
  19. {graphinate-0.4.0 → graphinate-0.5.0}/.github/workflows/publish-docs.yaml +0 -0
  20. {graphinate-0.4.0 → graphinate-0.5.0}/.github/workflows/publish.yml +0 -0
  21. {graphinate-0.4.0 → graphinate-0.5.0}/.github/workflows/test-beta.yml +0 -0
  22. {graphinate-0.4.0 → graphinate-0.5.0}/.github/workflows/test.yml +0 -0
  23. {graphinate-0.4.0 → graphinate-0.5.0}/.gitignore +0 -0
  24. {graphinate-0.4.0 → graphinate-0.5.0}/.sonarcloud.properties +0 -0
  25. {graphinate-0.4.0 → graphinate-0.5.0}/LICENSE +0 -0
  26. {graphinate-0.4.0 → graphinate-0.5.0}/STATS.md +0 -0
  27. {graphinate-0.4.0 → graphinate-0.5.0}/docs/acknowledge.md +0 -0
  28. {graphinate-0.4.0 → graphinate-0.5.0}/docs/assets/images/logo-128.png +0 -0
  29. {graphinate-0.4.0 → graphinate-0.5.0}/docs/assets/images/network_graph.png +0 -0
  30. {graphinate-0.4.0 → graphinate-0.5.0}/docs/assets/stylesheets/extra.css +0 -0
  31. {graphinate-0.4.0 → graphinate-0.5.0}/docs/dev.md +0 -0
  32. {graphinate-0.4.0 → graphinate-0.5.0}/docs/examples/code.md +0 -0
  33. {graphinate-0.4.0 → graphinate-0.5.0}/docs/examples/github.md +0 -0
  34. {graphinate-0.4.0 → graphinate-0.5.0}/docs/examples/math.md +0 -0
  35. {graphinate-0.4.0 → graphinate-0.5.0}/docs/examples/system.md +0 -0
  36. {graphinate-0.4.0 → graphinate-0.5.0}/docs/examples/web.md +0 -0
  37. {graphinate-0.4.0 → graphinate-0.5.0}/docs/gen_ref_pages.py +0 -0
  38. {graphinate-0.4.0 → graphinate-0.5.0}/docs/index.md +0 -0
  39. {graphinate-0.4.0 → graphinate-0.5.0}/docs/intro.md +0 -0
  40. {graphinate-0.4.0 → graphinate-0.5.0}/docs/start.md +0 -0
  41. {graphinate-0.4.0 → graphinate-0.5.0}/docs/usage/cli.md +0 -0
  42. {graphinate-0.4.0 → graphinate-0.5.0}/docs/usage/lib.md +0 -0
  43. {graphinate-0.4.0 → graphinate-0.5.0}/examples/code/python_ast.py +0 -0
  44. {graphinate-0.4.0 → graphinate-0.5.0}/examples/code/python_dependencies.py +0 -0
  45. {graphinate-0.4.0 → graphinate-0.5.0}/examples/code/requirements.txt +0 -0
  46. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/_client.py +0 -0
  47. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/commits_visibilty_graph.py +0 -0
  48. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/followers.graphql +0 -0
  49. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/followers.py +0 -0
  50. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/graphql.config.yml +0 -0
  51. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/repositories.graphql +0 -0
  52. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/repositories.py +0 -0
  53. {graphinate-0.4.0 → graphinate-0.5.0}/examples/github/requirements.txt +0 -0
  54. {graphinate-0.4.0 → graphinate-0.5.0}/examples/math/polygonal_graph.py +0 -0
  55. {graphinate-0.4.0 → graphinate-0.5.0}/examples/math/requirements.txt +0 -0
  56. {graphinate-0.4.0 → graphinate-0.5.0}/examples/system/processes.py +0 -0
  57. {graphinate-0.4.0 → graphinate-0.5.0}/examples/system/requirements.txt +0 -0
  58. {graphinate-0.4.0 → graphinate-0.5.0}/examples/web/requirements.txt +0 -0
  59. {graphinate-0.4.0 → graphinate-0.5.0}/mkdocs.yml +0 -0
  60. {graphinate-0.4.0 → graphinate-0.5.0}/playground/ethernet/traceroute.py +0 -0
  61. {graphinate-0.4.0 → graphinate-0.5.0}/playground/genric_graph.graphql +0 -0
  62. {graphinate-0.4.0 → graphinate-0.5.0}/playground/graphql.config.yml +0 -0
  63. {graphinate-0.4.0 → graphinate-0.5.0}/playground/house_of_graphs.py +0 -0
  64. {graphinate-0.4.0 → graphinate-0.5.0}/playground/social/albums.json +0 -0
  65. {graphinate-0.4.0 → graphinate-0.5.0}/playground/social/musicisians.py +0 -0
  66. {graphinate-0.4.0 → graphinate-0.5.0}/playground/text/nlp_graph.py +0 -0
  67. {graphinate-0.4.0 → graphinate-0.5.0}/playground/text/requirements.txt +0 -0
  68. {graphinate-0.4.0 → graphinate-0.5.0}/playground/time_series/requirements.txt +0 -0
  69. {graphinate-0.4.0 → graphinate-0.5.0}/playground/time_series/visibility_graph.py +0 -0
  70. {graphinate-0.4.0 → graphinate-0.5.0}/sonar-project.properties +0 -0
  71. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/__init__.py +0 -0
  72. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/__main__.py +0 -0
  73. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/cli.py +0 -0
  74. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/color.py +0 -0
  75. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/materializers/__init__.py +0 -0
  76. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/materializers/matplotlib.py +0 -0
  77. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/__init__.py +0 -0
  78. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/starlette/__init__.py +0 -0
  79. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/starlette/views.py +0 -0
  80. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/__init__.py +0 -0
  81. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/elements/__init__.py +0 -0
  82. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/elements/index.html +0 -0
  83. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/graphiql/__init__.py +0 -0
  84. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/graphiql/index.html +0 -0
  85. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/rapidoc/__init__.py +0 -0
  86. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/rapidoc/index.html +0 -0
  87. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/static/images/logo-128.png +0 -0
  88. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/static/images/network_graph.png +0 -0
  89. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/viewer/__init__.py +0 -0
  90. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/voyager/__init__.py +0 -0
  91. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/server/web/voyager/index.html +0 -0
  92. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/tools/__init__.py +0 -0
  93. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/tools/converters.py +0 -0
  94. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/tools/mutators.py +0 -0
  95. {graphinate-0.4.0 → graphinate-0.5.0}/src/graphinate/typing.py +0 -0
  96. {graphinate-0.4.0 → graphinate-0.5.0}/tests/conftest.py +0 -0
  97. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_builders.py +0 -0
  98. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_cli.py +0 -0
  99. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_color.py +0 -0
  100. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_materializers.py +0 -0
  101. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_modeling.py +0 -0
  102. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_server.py +0 -0
  103. {graphinate-0.4.0 → graphinate-0.5.0}/tests/graphinate/test_tools.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Graphinate
3
- Version: 0.4.0
4
- Summary: Graphinate. Data to Graphs.
3
+ Version: 0.5.0
4
+ Summary: 𝔾raphinate. Data to Graphs.
5
5
  Project-URL: Homepage, https://erivlis.github.io/graphinate
6
6
  Project-URL: Documentation, https://erivlis.github.io/graphinate
7
7
  Project-URL: Bug Tracker, https://github.com/erivlis/graphinate/issues
@@ -58,7 +58,7 @@ Requires-Dist: pytest-randomly; extra == 'test'
58
58
  Requires-Dist: pytest-xdist; extra == 'test'
59
59
  Description-Content-Type: text/markdown
60
60
 
61
- # [Graphinate. Data to Graphs.](https://erivlis.github.io/graphinate/)
61
+ # [𝔾raphinate. Data to Graphs.](https://erivlis.github.io/graphinate/)
62
62
 
63
63
  <img height="360" src="https://github.com/erivlis/graphinate/assets/9897520/dae41f9f-69e5-4eb5-a488-87ce7f51fa32" alt="Graphinate. Data to Graphs.">
64
64
 
@@ -120,10 +120,14 @@ Description-Content-Type: text/markdown
120
120
  <a href="https://snyk.io/test/github/erivlis/graphinate"><img alt="Snyk" src="https://snyk.io/test/github/erivlis/Graphinate/badge.svg"></a>
121
121
  </td>
122
122
  </tr>
123
+ <tr>
124
+ <td>Badge</td>
125
+ <td>
126
+ <a href="https://img.shields.io/badge/%F0%9D%94%BE%3D%7B%F0%9D%95%8D%2C%F0%9D%94%BC%7D-Graphinate-darkviolet"><img alt="Graphinate" src="https://img.shields.io/badge/%F0%9D%94%BE%3D%7B%F0%9D%95%8D%2C%F0%9D%94%BC%7D-Graphinate-darkviolet"></a>
127
+ </td>
128
+ </tr>
123
129
  </table>
124
130
 
125
- ---------------------
126
-
127
131
  ## Introduction
128
132
 
129
133
  ### What is Graphinate?
@@ -1,4 +1,4 @@
1
- # [Graphinate. Data to Graphs.](https://erivlis.github.io/graphinate/)
1
+ # [𝔾raphinate. Data to Graphs.](https://erivlis.github.io/graphinate/)
2
2
 
3
3
  <img height="360" src="https://github.com/erivlis/graphinate/assets/9897520/dae41f9f-69e5-4eb5-a488-87ce7f51fa32" alt="Graphinate. Data to Graphs.">
4
4
 
@@ -60,10 +60,14 @@
60
60
  <a href="https://snyk.io/test/github/erivlis/graphinate"><img alt="Snyk" src="https://snyk.io/test/github/erivlis/Graphinate/badge.svg"></a>
61
61
  </td>
62
62
  </tr>
63
+ <tr>
64
+ <td>Badge</td>
65
+ <td>
66
+ <a href="https://img.shields.io/badge/%F0%9D%94%BE%3D%7B%F0%9D%95%8D%2C%F0%9D%94%BC%7D-Graphinate-darkviolet"><img alt="Graphinate" src="https://img.shields.io/badge/%F0%9D%94%BE%3D%7B%F0%9D%95%8D%2C%F0%9D%94%BC%7D-Graphinate-darkviolet"></a>
67
+ </td>
68
+ </tr>
63
69
  </table>
64
70
 
65
- ---------------------
66
-
67
71
  ## Introduction
68
72
 
69
73
  ### What is Graphinate?
@@ -0,0 +1,69 @@
1
+ import operator
2
+
3
+ import graphs
4
+ import networkx as nx
5
+
6
+ import graphinate
7
+ from graphinate.materializers import Materializers
8
+
9
+
10
+ def model(items: list[tuple[str, nx.Graph]]) -> graphinate.GraphModel:
11
+ """
12
+ Generate a graph model based on the provided iterable of graphs.
13
+ The function creates a graph model named 'Graph Atlas' using the 'graphinate' library.
14
+ It then combines all the graphs from the input iterable into a single disjoint union graph using NetworkX library.
15
+ The function defines edges for the combined graph by iterating over all edges in the disjoint union graph and
16
+ yielding dictionaries with 'source' and 'target' keys representing the edge connections.
17
+ Finally, the function yields the created graph model containing the combined graph with defined edges.
18
+
19
+ Args:
20
+ items: A list containing graphs to be combined into a single graph model.
21
+
22
+ Yields:
23
+ GraphModel: A graph model containing the combined graph with defined edges.
24
+ """
25
+
26
+ def items_iter(recs):
27
+ for name, g in recs:
28
+ print(name)
29
+ yield g
30
+
31
+ g = nx.disjoint_union_all(items_iter(items)) if len(items) > 1 else items[0][1]
32
+
33
+ graph_model = graphinate.model('Graph Atlas')
34
+
35
+ @graph_model.node(operator.itemgetter(1),
36
+ key=operator.itemgetter(0),
37
+ value=operator.itemgetter(0))
38
+ def nodes():
39
+ yield from g.nodes(data='type')
40
+
41
+ @graph_model.edge(operator.itemgetter(2),
42
+ source=operator.itemgetter(0),
43
+ target=operator.itemgetter(1),
44
+ label=operator.itemgetter(0, 1),
45
+ value=operator.itemgetter(0, 1))
46
+ def edge():
47
+ yield from g.edges.data('type')
48
+
49
+ return graph_model
50
+
51
+
52
+ if __name__ == '__main__':
53
+ from gui import ListboxChooser, RadiobuttonChooser
54
+
55
+ graph_atlas = graphs.atlas()
56
+
57
+ listbox_chooser = ListboxChooser('Choose Graph', graph_atlas)
58
+ choices = list(listbox_chooser.get_choices())
59
+ model = model(choices)
60
+
61
+ # or
62
+ # model(graph_atlas.values())
63
+
64
+ radiobutton_chooser = RadiobuttonChooser('Choose Materializer',
65
+ options={m.name: m.value for m in Materializers},
66
+ default=(None, None))
67
+ result = radiobutton_chooser.get_choice()
68
+ builder, handler = result[1]
69
+ graphinate.materialize(model, builder=builder, builder_output_handler=handler)
@@ -1,4 +1,5 @@
1
1
  import itertools
2
+ import re
2
3
  from collections.abc import Iterable
3
4
  from typing import NewType
4
5
 
@@ -692,18 +693,20 @@ SPECIAL_GRAPHS_ADJACENCY_LISTS = {
692
693
  AdjacencyList = NewType('AdjacencyList', dict[int, list[int]])
693
694
 
694
695
 
695
- def ladder_ring_graph(size: int):
696
- ring = nx.ladder_graph(size)
697
- ring.add_edge(size, size * 2 - 1)
698
- ring.add_edge(0, size - 1)
699
- return ladder_ring_graph
696
+ def ladder_ring_graph(size: int) -> nx.Graph:
697
+ g: nx.Graph = nx.ladder_graph(size)
698
+ g.add_edge(size, size * 2 - 1)
699
+ g.add_edge(0, size - 1)
700
+ g.name = f'Ladder Ring[{size}]'
701
+ return g
700
702
 
701
703
 
702
- def ladder_mobius_graph(size: int):
703
- mobius = nx.ladder_graph(size)
704
- mobius.add_edge(size, size - 1)
705
- mobius.add_edge(0, size * 2 - 1)
706
- return mobius
704
+ def ladder_mobius_graph(size: int) -> nx.Graph:
705
+ g = nx.ladder_graph(size)
706
+ g.add_edge(size, size - 1)
707
+ g.add_edge(0, size * 2 - 1)
708
+ g.name = f'Ladder Möbius Ring[{size}]'
709
+ return g
707
710
 
708
711
 
709
712
  def _cylinder_edges(circumference: int, length: int) -> Iterable[tuple[int, int]]:
@@ -721,7 +724,9 @@ def _cylinder_edges(circumference: int, length: int) -> Iterable[tuple[int, int]
721
724
 
722
725
 
723
726
  def cylinder_graph(circumference: int, length: int) -> nx.Graph:
724
- return nx.Graph(_cylinder_edges(circumference, length))
727
+ g = nx.Graph(_cylinder_edges(circumference, length))
728
+ g.name = f'Cylinder[circumference={circumference},length={length}]'
729
+ return g
725
730
 
726
731
 
727
732
  def _spiral_edges(n, k) -> Iterable[tuple[int, int]]:
@@ -733,7 +738,9 @@ def _spiral_edges(n, k) -> Iterable[tuple[int, int]]:
733
738
 
734
739
 
735
740
  def spiral_graph(n, k) -> nx.Graph:
736
- return nx.Graph(_spiral_edges(n, k))
741
+ g = nx.Graph(_spiral_edges(n, k))
742
+ g.name = f'Spiral[n={n},k={k}]'
743
+ return g
737
744
 
738
745
 
739
746
  def _spiral_torus_edges(n, k) -> Iterable[tuple[int, int]]:
@@ -744,7 +751,20 @@ def _spiral_torus_edges(n, k) -> Iterable[tuple[int, int]]:
744
751
 
745
752
 
746
753
  def spiral_torus_graph(n, k) -> nx.Graph:
747
- return nx.Graph(_spiral_torus_edges(n, k))
754
+ g = nx.Graph(_spiral_torus_edges(n, k))
755
+ g.name = f'Spiral Torus[n={n},k={k}]'
756
+ return g
757
+
758
+
759
+ def k_regular_edges(n, k) -> Iterable[tuple[int, int]]:
760
+ yield from itertools.chain.from_iterable(
761
+ ((i, j) for j in range(i + 1, i + k + 1))
762
+ for i in range(n-k))
763
+
764
+ def k_regular_graph(n, k) -> nx.Graph:
765
+ g = nx.Graph(k_regular_edges(n, k))
766
+ g.name = f'Generalized Buckyball[n={n},k={k}]'
767
+ return g
748
768
 
749
769
 
750
770
  def adjacency_edges(adjacency_list: AdjacencyList) -> Iterable[tuple[int, int]]:
@@ -786,23 +806,57 @@ def atlas():
786
806
  """
787
807
 
788
808
  graph_atlas = {
809
+ 'Triangle': nx.cycle_graph(3),
810
+ 'Square': nx.cycle_graph(4),
811
+ 'Square Lattice[3,3]': nx.grid_2d_graph(3, 3),
812
+ 'Pentagon': nx.cycle_graph(5),
813
+ 'Hexagon': nx.cycle_graph(6),
814
+ 'Heptagon': nx.cycle_graph(7),
815
+ 'Octagon': nx.cycle_graph(8),
789
816
  'Tetrahedron': nx.tetrahedral_graph(),
790
817
  'Cube': nx.hypercube_graph(3),
791
818
  'Octahedron': nx.octahedral_graph(),
792
819
  'Dodecahedron': nx.dodecahedral_graph(),
793
820
  'Icosahedron': nx.icosahedral_graph(),
794
821
  'Tesseract': nx.hypercube_graph(4),
822
+ 'Hypercube[5]': nx.hypercube_graph(5),
795
823
  'Truncated Cube': nx.truncated_cube_graph(),
796
824
  'Truncated Tetrahedron': nx.truncated_tetrahedron_graph(),
797
- 'Ladder': nx.ladder_graph(16),
798
- 'Ring': ladder_ring_graph(16),
799
- 'Möbius': ladder_mobius_graph(16),
800
- 'Cylinder': cylinder_graph(6, 8),
801
- 'Spiral': spiral_graph(18, 8),
802
- 'Spiral Torus': spiral_torus_graph(128, 8),
803
- 'Circulant[10,[2]]': nx.circulant_graph(10, [2])
825
+ 'Ladder[16]': nx.ladder_graph(16),
826
+ 'Ladder Ring[16]': ladder_ring_graph(16),
827
+ 'Ladder Möbius Ring[16]': ladder_mobius_graph(16),
828
+ 'Cylinder[6,8]': cylinder_graph(6, 8),
829
+ 'Spiral[128,8]': spiral_graph(128, 8),
830
+ 'Spiral Torus[128,8]': spiral_torus_graph(128, 8),
831
+ 'Chvátal': nx.chvatal_graph(),
832
+ 'Circulant[10,[2]]': nx.circulant_graph(10, [2]),
833
+ 'Desargues': nx.desargues_graph(),
834
+ 'Dorogovtsev-Goltsev-Mendes[4]': nx.dorogovtsev_goltsev_mendes_graph(4),
835
+ 'Frucht': nx.frucht_graph(),
836
+ 'Heawood': nx.heawood_graph(),
837
+ 'Hoffman-Singleton': nx.hoffman_singleton_graph(),
838
+ # 'Margulis-Gabber-Galil[8]': nx.margulis_gabber_galil_graph(8),
839
+ 'Papus': nx.pappus_graph(),
840
+ 'Petersen': nx.petersen_graph(),
841
+ 'Sedgewick Maze': nx.sedgewick_maze_graph(),
842
+ 'Tutte': nx.tutte_graph(),
804
843
  }
805
844
 
806
845
  graph_atlas.update(special_graphs())
807
846
 
847
+ def clean(s: str):
848
+ # Remove invalid characters
849
+ s = re.sub('[^0-9a-zA-Z_]', '_', s)
850
+
851
+ # Remove leading characters until we find a letter or underscore
852
+ s = re.sub('^[^a-zA-Z_]+', '', s)
853
+
854
+ return s
855
+
856
+ for name, g in graph_atlas.items():
857
+ _type = clean(name).capitalize()
858
+ nx.set_node_attributes(g, _type, 'type')
859
+ nx.set_edge_attributes(g, _type, 'type')
860
+
808
861
  return graph_atlas
862
+
@@ -0,0 +1,108 @@
1
+ import platform
2
+ import tkinter as tk
3
+ from tkinter import ttk
4
+
5
+
6
+ class ModalWindow(tk.Tk):
7
+ def __init__(self, title: str):
8
+ super().__init__()
9
+ self.title(title)
10
+ self.configure_window()
11
+
12
+ def configure_window(self):
13
+ if platform.system().lower() == 'windows':
14
+ self.wm_attributes('-toolwindow', 'True')
15
+ self.wm_attributes('-topmost', 'True')
16
+ self.resizable(False, False)
17
+
18
+
19
+ class RadiobuttonChooser(ModalWindow):
20
+ """
21
+ Usage Example:
22
+ ```python
23
+ radiobutton_chooser = RadiobuttonChooser("Choose an option", {"Option 1": 1, "Option 2": 2})
24
+ choice, value = radiobutton_chooser.get_choice()
25
+ print(choice, value)
26
+ ```
27
+ """
28
+
29
+ def __init__(self, window_title: str, options: dict, default=None):
30
+ super().__init__(window_title)
31
+ self.exit_button = None
32
+ self.choice_var = tk.StringVar(self, None)
33
+ self.default = default
34
+ self.options = options
35
+ self.create_widgets()
36
+ self.mainloop()
37
+
38
+ def create_widgets(self):
39
+ frame = ttk.Frame(self, borderwidth=5, relief='solid')
40
+ frame.pack(padx=4, pady=4)
41
+
42
+ ttk.Label(frame, text="Output Mode:").pack()
43
+
44
+ for option in self.options:
45
+ ttk.Radiobutton(
46
+ frame,
47
+ text=option,
48
+ variable=self.choice_var,
49
+ value=option,
50
+ command=self.enable_exit_button,
51
+ padding=4
52
+ ).pack(side=tk.TOP, anchor="w")
53
+
54
+ self.exit_button = ttk.Button(self, text="OK", command=self.destroy, state=tk.DISABLED)
55
+ self.exit_button.pack(pady=4, side=tk.BOTTOM)
56
+
57
+ def enable_exit_button(self):
58
+ self.exit_button['state'] = tk.NORMAL
59
+
60
+ def get_choice(self):
61
+ return self.choice_var.get(), self.options.get(self.choice_var.get(), self.default)
62
+
63
+
64
+ class ListboxChooser(ModalWindow):
65
+ """
66
+ Usage Example:
67
+
68
+ ```python
69
+ listbox_chooser = ListboxChooser("Choose options", {"Option 1": 1, "Option 2": 2, "Option 3": 3})
70
+ for choice, value in listbox_chooser.get_choices():
71
+ print(choice, value)
72
+ ```
73
+ """
74
+
75
+ def __init__(self, window_title: str, options: dict, default=None):
76
+ super().__init__(window_title)
77
+ self.exit_button = None
78
+ self.choices = list(options.keys())
79
+ self.options = options
80
+ self.default = default
81
+ self.selection_var = tk.Variable(self)
82
+ self.create_widgets()
83
+ self.mainloop()
84
+
85
+ def create_widgets(self):
86
+ frame = ttk.Frame(self)
87
+ frame.pack(padx=2, pady=2)
88
+
89
+ listbox = tk.Listbox(frame, listvariable=tk.Variable(self, value=self.choices), selectmode="multiple",
90
+ height=min(len(self.choices), 50), width=max(len(c) for c in self.choices))
91
+ listbox.pack(side=tk.LEFT, fill=tk.BOTH)
92
+
93
+ scrollbar = ttk.Scrollbar(frame, command=listbox.yview)
94
+ scrollbar.pack(side=tk.RIGHT, fill=tk.BOTH)
95
+ listbox.config(yscrollcommand=scrollbar.set)
96
+
97
+ listbox.bind("<<ListboxSelect>>", self.on_select)
98
+
99
+ self.exit_button = ttk.Button(self, text="OK", command=self.destroy, state=tk.DISABLED)
100
+ self.exit_button.pack(pady=4, side=tk.BOTTOM)
101
+
102
+ def on_select(self, event):
103
+ self.exit_button['state'] = tk.NORMAL
104
+ self.selection_var.set(event.widget.curselection())
105
+
106
+ def get_choices(self):
107
+ for choice in self.selection_var.get():
108
+ yield self.choices[choice], self.options.get(self.choices[choice], self.default)
@@ -1,4 +1,4 @@
1
- from urllib.parse import urljoin, urlparse
1
+ from urllib.parse import urlparse
2
2
 
3
3
  import requests
4
4
  from bs4 import BeautifulSoup
@@ -28,13 +28,17 @@ def page_links_graph_model(max_depth: int = DEFAULT_MAX_DEPTH):
28
28
  for link in soup.find_all('a', href=True):
29
29
  child_url = link.get('href')
30
30
 
31
- if child_url.startswith('javascript:'):
31
+ if child_url.startswith('javascript:'): # Skip JavaScript links
32
32
  continue
33
- elif child_url.startswith('//'):
33
+
34
+ if child_url.startswith('//'): # Handle protocol-relative URLs
34
35
  child_url = f"https:{child_url}"
35
- elif not bool(urlparse(child_url).netloc):
36
- child_url = urljoin(url, child_url)
37
- elif not child_url.startswith('http'):
36
+
37
+ if not bool(urlparse(child_url).netloc): # Skip relative URLs
38
+ # child_url = urljoin(url, child_url)
39
+ continue
40
+
41
+ if not child_url.startswith('http'): # Skip non-HTTP URLs
38
42
  continue
39
43
 
40
44
  yield {'source': url, 'target': child_url}
@@ -62,7 +66,7 @@ if __name__ == '__main__':
62
66
  model=model,
63
67
  graph_type=graphinate.GraphType.DiGraph,
64
68
  default_node_attributes={'type': 'url'},
65
- builder=graphinate.builders.NetworkxBuilder,
66
- builder_output_handler=graphinate.materializers.matplotlib.plot,
69
+ builder=graphinate.builders.GraphQLBuilder,
70
+ builder_output_handler=graphinate.graphql,
67
71
  **params
68
72
  )
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "Graphinate"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  authors = [
5
5
  { name = "Eran Rivlis", email = "eran@rivlis.info" },
6
6
  ]
7
- description = "Graphinate. Data to Graphs."
7
+ description = "𝔾raphinate. Data to Graphs."
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [
@@ -203,7 +203,7 @@ class NetworkxBuilder(Builder):
203
203
  if node_type == 'tuple':
204
204
  node_type = node_model.type.lower()
205
205
 
206
- logger.debug("Adding node: '{}'", node_id)
206
+ logger.debug("Adding node: '{}'", node.value)
207
207
 
208
208
  if node_id in self._graph:
209
209
  self._graph.nodes[node_id]['value'].append(node.value)
@@ -231,16 +231,16 @@ class NetworkxBuilder(Builder):
231
231
  self._populate_node_type(node_model.type, **new_kwargs)
232
232
 
233
233
  def _populate_edges(self, **kwargs):
234
- for edge_type, edge_generators in self.model.edge_generators.items():
234
+ for edge_model, edge_generators in self.model.edge_generators.items():
235
235
  for edge_generator in edge_generators:
236
236
  for edge in edge_generator(**kwargs):
237
237
  edge_id = ((edge.source,), (edge.target,))
238
238
  edge_weight = edge.weight or 1.0
239
- logger.debug("Adding edge from: '{}' to: '{}'", edge.source, edge.target)
239
+ edge_type = edge.type.lower()
240
+ logger.debug("Adding edge from: '{}' to: '{}'", *edge_id)
240
241
 
241
242
  if isinstance(self._graph, nx.MultiGraph) or edge_id not in self._graph.edges:
242
- self._graph.add_edge((edge.source,),
243
- (edge.target,),
243
+ self._graph.add_edge(*edge_id,
244
244
  label=edge.label,
245
245
  type=edge_type,
246
246
  value=[edge.value],
@@ -4,7 +4,7 @@ from collections.abc import Callable, Iterable, Mapping
4
4
  from dataclasses import dataclass
5
5
  from typing import Any, Optional, Union
6
6
 
7
- from .typing import Edge, Edges, Element, Extractor, Items, Node, Nodes, NodeTypeAbsoluteId, UniverseNode
7
+ from .typing import Edge, Element, Extractor, Items, Node, NodeTypeAbsoluteId, UniverseNode
8
8
 
9
9
 
10
10
  class GraphModelError(Exception):
@@ -59,17 +59,14 @@ def elements(iterable: Iterable[Any],
59
59
  Returns:
60
60
  Iterable of Elements.
61
61
  """
62
+ for item in iterable:
63
+ _type = element_type(item) if element_type and callable(element_type) else element_type
64
+ if not _type.isidentifier():
65
+ raise ValueError(f"Invalid Type: {_type}. Must be a valid Python identifier.")
62
66
 
63
- if element_type and callable(element_type):
64
- for item in iterable:
65
- create_element = element(element_type(item), getters.keys())
66
- kwargs = {k: extractor(item, v) for k, v in getters.items()}
67
- yield create_element(**kwargs)
68
- else:
69
- create_element = element(element_type, getters.keys())
70
- for item in iterable:
71
- kwargs = {k: extractor(item, v) for k, v in getters.items()}
72
- yield create_element(**kwargs)
67
+ create_element = element(_type, getters.keys())
68
+ kwargs = {k: extractor(item, v) for k, v in getters.items()}
69
+ yield create_element(**kwargs)
73
70
 
74
71
 
75
72
  @dataclass
@@ -92,7 +89,7 @@ class NodeModel:
92
89
  parent_type: Optional[str] = UniverseNode
93
90
  uniqueness: bool = False
94
91
  parameters: set[str] | None = None
95
- generator: Nodes | None = None
92
+ generator: Callable[[], Iterable[Node]] | None = None
96
93
  label: Callable[[Any], str | None] = None
97
94
 
98
95
  @property
@@ -114,7 +111,7 @@ class GraphModel:
114
111
  self.name: str = name
115
112
  self._node_models: dict[NodeTypeAbsoluteId, NodeModel] = {}
116
113
  self._node_children: dict[str, list[str]] = defaultdict(list)
117
- self._edge_generators: dict[str, list[Edges]] = defaultdict(list)
114
+ self._edge_generators: dict[str, list[Callable[[], Iterable[Edge]]]] = defaultdict(list)
118
115
  self._networkx_graph = None
119
116
 
120
117
  def __add__(self, other: 'GraphModel'):
@@ -225,7 +222,7 @@ class GraphModel:
225
222
  return register_node
226
223
 
227
224
  def edge(self,
228
- _type: Optional[str] = None,
225
+ _type: Optional[Extractor] = None,
229
226
  source: Extractor = 'source',
230
227
  target: Extractor = 'target',
231
228
  label: Optional[Extractor] = str,
@@ -252,6 +249,8 @@ class GraphModel:
252
249
  edge_type = _type or f.__name__
253
250
  self._validate_type(edge_type)
254
251
 
252
+ model_type = f.__name__ if callable(edge_type) else edge_type
253
+
255
254
  getters = {
256
255
  'source': source,
257
256
  'target': target,
@@ -264,7 +263,7 @@ class GraphModel:
264
263
  def edge_generator(**kwargs) -> Iterable[Edge]:
265
264
  yield from elements(f(**kwargs), edge_type, **getters)
266
265
 
267
- self._edge_generators[edge_type].append(edge_generator)
266
+ self._edge_generators[model_type].append(edge_generator)
268
267
 
269
268
  return register_edge
270
269
 
@@ -0,0 +1,50 @@
1
+ <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Filters for shadow -->
3
+ <defs>
4
+ <filter id="circle-shadow" x="-50%" y="-50%" width="200%" height="500%">
5
+ <feDropShadow dx="6" dy="6" stdDeviation="1" flood-color="rgba(0, 0, 0, 0.8)"/>
6
+ </filter>
7
+ <filter id="text-shadow" x="-50%" y="-50%" width="200%" height="500%">
8
+ <feDropShadow dx="5" dy="5" stdDeviation="1" flood-color="rgba(0, 0, 0, 0.6)"/>
9
+ </filter>
10
+ </defs>
11
+
12
+ <text x="10" y="30"
13
+ font-family="serif"
14
+ font-size="40"
15
+ font-weight="500"
16
+ fill="darkviolet"
17
+ stroke="indigo"
18
+ stroke-width="0.5"
19
+ filter="url(#text-shadow)">&#120126;raphinate
20
+ </text>
21
+ <!-- Background -->
22
+ <!--rect width="100%" height="100%" fill="#2E004F" /-->
23
+
24
+ <!-- Circle with shadow -->
25
+ <circle cx="100" cy="100" r="60" fill="indigo" stroke="rebeccapurple" stroke-width="0.5"
26
+ filter="url(#circle-shadow)"/>
27
+ <!-- Double-struck G with shadow -->
28
+ <text x="99" y="113"
29
+ font-family="serif"
30
+ font-size="108"
31
+ fill="darkviolet"
32
+ text-anchor="middle"
33
+ stroke="indigo"
34
+ stroke-width="0.5"
35
+ alignment-baseline="middle"
36
+ filter="url(#text-shadow)">
37
+ &#120126;
38
+ </text>
39
+
40
+
41
+ <text x="10" y="190"
42
+ font-family="serif"
43
+ font-size="40"
44
+ font-weight="500"
45
+ fill="darkviolet"
46
+ stroke="indigo"
47
+ stroke-width="0.5"
48
+ filter="url(#text-shadow)">&#120126;raphinate
49
+ </text>
50
+ </svg>