Graphinate 0.6.0__tar.gz → 0.7.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 (121) hide show
  1. {graphinate-0.6.0 → graphinate-0.7.0}/.gitignore +2 -1
  2. {graphinate-0.6.0 → graphinate-0.7.0}/PKG-INFO +2 -1
  3. graphinate-0.7.0/docs/examples/social.md +15 -0
  4. graphinate-0.7.0/examples/code/html_dom.py +61 -0
  5. {graphinate-0.6.0 → graphinate-0.7.0}/examples/code/python_ast.py +3 -3
  6. graphinate-0.7.0/examples/code/tokens.py +53 -0
  7. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/repositories.py +2 -2
  8. {graphinate-0.6.0 → graphinate-0.7.0}/examples/math/graph_atlas.py +1 -1
  9. graphinate-0.7.0/examples/math/requirements.txt +2 -0
  10. graphinate-0.7.0/examples/social/cache/13/dd/73ce25face7beb30b69b64feeb77.val +0 -0
  11. graphinate-0.7.0/examples/social/cache/21/9e/00846f323987ba16cfbe0127d8eb.val +0 -0
  12. graphinate-0.7.0/examples/social/cache/70/b6/2aefb0269adce7fedf877fa0d267.val +0 -0
  13. graphinate-0.7.0/examples/social/cache/87/f5/ec1739bc369e84c3fcb302bf532a.val +0 -0
  14. graphinate-0.7.0/examples/social/cache/ba/fe/3aca7b2c38abff60e7ce5eb486a8.val +0 -0
  15. graphinate-0.7.0/examples/social/cache/c7/9e/ce82b0288020b7152779df09bd73.val +0 -0
  16. graphinate-0.7.0/examples/social/cache/cache.db +0 -0
  17. graphinate-0.7.0/examples/social/cache/d2/53/3b88f2fc162561cfdbbe9abc352a.val +0 -0
  18. graphinate-0.7.0/examples/social/cache/e2/d5/5d079f200eabf9b625b0473f6fbe.val +0 -0
  19. graphinate-0.7.0/examples/social/gui.py +108 -0
  20. graphinate-0.7.0/examples/social/music_artists.py +149 -0
  21. graphinate-0.7.0/examples/social/requirements.txt +3 -0
  22. graphinate-0.7.0/examples/system/.ignore +796 -0
  23. graphinate-0.7.0/examples/system/files.py +81 -0
  24. {graphinate-0.6.0 → graphinate-0.7.0}/examples/system/requirements.txt +1 -0
  25. {graphinate-0.6.0 → graphinate-0.7.0}/pyproject.toml +3 -2
  26. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/__init__.py +2 -1
  27. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/builders.py +60 -102
  28. graphinate-0.7.0/src/graphinate/constants.py +2 -0
  29. graphinate-0.7.0/src/graphinate/converters.py +91 -0
  30. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/modeling.py +53 -32
  31. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/viewer/index.html +164 -45
  32. graphinate-0.6.0/src/graphinate/tools/__init__.py → graphinate-0.7.0/src/graphinate/tools.py +3 -1
  33. {graphinate-0.6.0 → graphinate-0.7.0}/tests/conftest.py +6 -5
  34. {graphinate-0.6.0 → graphinate-0.7.0}/tests/graphinate/test_builders.py +25 -63
  35. graphinate-0.7.0/tests/graphinate/test_converters.py +60 -0
  36. {graphinate-0.6.0 → graphinate-0.7.0}/tests/graphinate/test_modeling.py +4 -3
  37. graphinate-0.6.0/examples/math/requirements.txt +0 -1
  38. graphinate-0.6.0/src/graphinate/tools/converters.py +0 -28
  39. graphinate-0.6.0/src/graphinate/tools/mutators.py +0 -153
  40. graphinate-0.6.0/tests/graphinate/test_tools.py +0 -65
  41. {graphinate-0.6.0 → graphinate-0.7.0}/.coveragerc +0 -0
  42. {graphinate-0.6.0 → graphinate-0.7.0}/.github/dependabot.yml +0 -0
  43. {graphinate-0.6.0 → graphinate-0.7.0}/.github/workflows/codeql.yml +0 -0
  44. {graphinate-0.6.0 → graphinate-0.7.0}/.github/workflows/publish-docs.yaml +0 -0
  45. {graphinate-0.6.0 → graphinate-0.7.0}/.github/workflows/publish.yml +0 -0
  46. {graphinate-0.6.0 → graphinate-0.7.0}/.github/workflows/test-beta.yml +0 -0
  47. {graphinate-0.6.0 → graphinate-0.7.0}/.github/workflows/test.yml +0 -0
  48. {graphinate-0.6.0 → graphinate-0.7.0}/.sonarcloud.properties +0 -0
  49. {graphinate-0.6.0 → graphinate-0.7.0}/LICENSE +0 -0
  50. {graphinate-0.6.0 → graphinate-0.7.0}/README.md +0 -0
  51. {graphinate-0.6.0 → graphinate-0.7.0}/STATS.md +0 -0
  52. {graphinate-0.6.0 → graphinate-0.7.0}/docs/acknowledge.md +0 -0
  53. {graphinate-0.6.0 → graphinate-0.7.0}/docs/assets/badge/v0.json +0 -0
  54. {graphinate-0.6.0 → graphinate-0.7.0}/docs/assets/images/logo-128.png +0 -0
  55. {graphinate-0.6.0 → graphinate-0.7.0}/docs/assets/images/network_graph.png +0 -0
  56. {graphinate-0.6.0 → graphinate-0.7.0}/docs/assets/stylesheets/extra.css +0 -0
  57. {graphinate-0.6.0 → graphinate-0.7.0}/docs/dev.md +0 -0
  58. {graphinate-0.6.0 → graphinate-0.7.0}/docs/examples/code.md +0 -0
  59. {graphinate-0.6.0 → graphinate-0.7.0}/docs/examples/github.md +0 -0
  60. {graphinate-0.6.0 → graphinate-0.7.0}/docs/examples/math.md +0 -0
  61. {graphinate-0.6.0 → graphinate-0.7.0}/docs/examples/system.md +0 -0
  62. {graphinate-0.6.0 → graphinate-0.7.0}/docs/examples/web.md +0 -0
  63. {graphinate-0.6.0 → graphinate-0.7.0}/docs/gen_ref_pages.py +0 -0
  64. {graphinate-0.6.0 → graphinate-0.7.0}/docs/index.md +0 -0
  65. {graphinate-0.6.0 → graphinate-0.7.0}/docs/intro.md +0 -0
  66. {graphinate-0.6.0 → graphinate-0.7.0}/docs/start.md +0 -0
  67. {graphinate-0.6.0 → graphinate-0.7.0}/docs/usage/cli.md +0 -0
  68. {graphinate-0.6.0 → graphinate-0.7.0}/docs/usage/lib.md +0 -0
  69. {graphinate-0.6.0 → graphinate-0.7.0}/examples/code/python_dependencies.py +0 -0
  70. {graphinate-0.6.0 → graphinate-0.7.0}/examples/code/requirements.txt +0 -0
  71. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/_client.py +0 -0
  72. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/commits_visibilty_graph.py +0 -0
  73. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/followers.graphql +0 -0
  74. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/followers.py +0 -0
  75. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/graphql.config.yml +0 -0
  76. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/repositories.graphql +0 -0
  77. {graphinate-0.6.0 → graphinate-0.7.0}/examples/github/requirements.txt +0 -0
  78. {graphinate-0.6.0 → graphinate-0.7.0}/examples/math/graphs.py +0 -0
  79. {graphinate-0.6.0 → graphinate-0.7.0}/examples/math/gui.py +0 -0
  80. {graphinate-0.6.0 → graphinate-0.7.0}/examples/math/polygonal_graph.py +0 -0
  81. {graphinate-0.6.0 → graphinate-0.7.0}/examples/system/processes.py +0 -0
  82. {graphinate-0.6.0 → graphinate-0.7.0}/examples/web/page_links.py +0 -0
  83. {graphinate-0.6.0 → graphinate-0.7.0}/examples/web/requirements.txt +0 -0
  84. {graphinate-0.6.0 → graphinate-0.7.0}/mkdocs.yml +0 -0
  85. {graphinate-0.6.0 → graphinate-0.7.0}/playground/ethernet/traceroute.py +0 -0
  86. {graphinate-0.6.0 → graphinate-0.7.0}/playground/genric_graph.graphql +0 -0
  87. {graphinate-0.6.0 → graphinate-0.7.0}/playground/graphql.config.yml +0 -0
  88. {graphinate-0.6.0 → graphinate-0.7.0}/playground/house_of_graphs.py +0 -0
  89. {graphinate-0.6.0 → graphinate-0.7.0}/playground/social/albums.json +0 -0
  90. {graphinate-0.6.0 → graphinate-0.7.0}/playground/social/musicisians.py +0 -0
  91. {graphinate-0.6.0 → graphinate-0.7.0}/playground/text/nlp_graph.py +0 -0
  92. {graphinate-0.6.0 → graphinate-0.7.0}/playground/text/requirements.txt +0 -0
  93. {graphinate-0.6.0 → graphinate-0.7.0}/playground/time_series/requirements.txt +0 -0
  94. {graphinate-0.6.0 → graphinate-0.7.0}/playground/time_series/visibility_graph.py +0 -0
  95. {graphinate-0.6.0 → graphinate-0.7.0}/sonar-project.properties +0 -0
  96. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/__main__.py +0 -0
  97. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/cli.py +0 -0
  98. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/color.py +0 -0
  99. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/materializers/__init__.py +0 -0
  100. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/materializers/matplotlib.py +0 -0
  101. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/__init__.py +0 -0
  102. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/starlette/__init__.py +0 -0
  103. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/starlette/views.py +0 -0
  104. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/__init__.py +0 -0
  105. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/elements/__init__.py +0 -0
  106. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/elements/index.html +0 -0
  107. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/graphiql/__init__.py +0 -0
  108. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/graphiql/index.html +0 -0
  109. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/rapidoc/__init__.py +0 -0
  110. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/rapidoc/index.html +0 -0
  111. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/static/images/logo-128.png +0 -0
  112. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/static/images/logo.svg +0 -0
  113. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/static/images/network_graph.png +0 -0
  114. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/viewer/__init__.py +0 -0
  115. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/voyager/__init__.py +0 -0
  116. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/server/web/voyager/index.html +0 -0
  117. {graphinate-0.6.0 → graphinate-0.7.0}/src/graphinate/typing.py +0 -0
  118. {graphinate-0.6.0 → graphinate-0.7.0}/tests/graphinate/test_cli.py +0 -0
  119. {graphinate-0.6.0 → graphinate-0.7.0}/tests/graphinate/test_color.py +0 -0
  120. {graphinate-0.6.0 → graphinate-0.7.0}/tests/graphinate/test_materializers.py +0 -0
  121. {graphinate-0.6.0 → graphinate-0.7.0}/tests/graphinate/test_server.py +0 -0
@@ -781,4 +781,5 @@ cython_debug/
781
781
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
782
782
  #.idea/
783
783
 
784
- /.idea/**
784
+ /.idea/**
785
+ .qodo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Graphinate
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
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
@@ -30,6 +30,7 @@ Requires-Python: >=3.10
30
30
  Requires-Dist: click
31
31
  Requires-Dist: inflect
32
32
  Requires-Dist: loguru
33
+ Requires-Dist: mappingtools
33
34
  Requires-Dist: matplotlib
34
35
  Requires-Dist: networkx
35
36
  Requires-Dist: strawberry-graphql[asgi,opentelemetry]
@@ -0,0 +1,15 @@
1
+ # Social
2
+
3
+ ## Music
4
+
5
+ === "Music"
6
+
7
+ ``` python title="examples/social/music.py" linenums="1"
8
+ --8<-- "examples/social/music.py"
9
+ ```
10
+
11
+ === "Dependencies"
12
+
13
+ ``` text title="examples/social/requirements.txt" linenums="1"
14
+ --8<-- "examples/social/requirements.txt"
15
+ ```
@@ -0,0 +1,61 @@
1
+ import base64
2
+
3
+ import requests
4
+ from bs4 import BeautifulSoup, Tag
5
+
6
+ import graphinate
7
+
8
+
9
+ def load_html_from_url(url="https://www.google.com"):
10
+ response = requests.get(url)
11
+ return response.text
12
+
13
+
14
+ def load_html(file_path):
15
+ with open(file_path) as file:
16
+ return file.read()
17
+
18
+
19
+ def html_dom_graph_model(html_content):
20
+ graph_model = graphinate.model(name="HTML DOM Graph")
21
+ soup = BeautifulSoup(html_content, 'html.parser')
22
+
23
+ def node_type(tag: Tag):
24
+ return tag.name.strip('[]')
25
+
26
+ def node_key(tag: Tag):
27
+ return str((tag.sourceline, tag.sourcepos)) if isinstance(tag, Tag) else base64.b64encode(
28
+ tag.encode()).decode()
29
+
30
+ def node_label(tag: Tag):
31
+ return str(tag)
32
+
33
+ @graph_model.node(node_type, key=node_key, label=node_label)
34
+ def html_node():
35
+ for tag in soup.descendants:
36
+ if tag.name is not None:
37
+ yield tag
38
+
39
+ @graph_model.edge()
40
+ def contains():
41
+ for tag in soup.descendants:
42
+ if tag.name is not None:
43
+ for child in tag.children:
44
+ if child.name is not None:
45
+ yield {
46
+ 'source': node_key(tag),
47
+ 'target': node_key(child)
48
+ }
49
+
50
+ return graph_model
51
+
52
+
53
+ if __name__ == '__main__':
54
+ html_content = load_html_from_url()
55
+ dom_model = html_dom_graph_model(html_content)
56
+ graphinate.materialize(
57
+ dom_model,
58
+ builder=graphinate.builders.GraphQLBuilder,
59
+ builder_output_handler=graphinate.graphql
60
+ )
61
+
@@ -79,14 +79,14 @@ def ast_graph_model():
79
79
  def target(value):
80
80
  return endpoint(value, 'target')
81
81
 
82
- @graph_model.node(_type=node_type,
82
+ @graph_model.node(type_=node_type,
83
83
  key=key,
84
84
  label=node_label,
85
- uniqueness=True)
85
+ unique=True)
86
86
  def ast_node(**kwargs):
87
87
  yield from _ast_nodes([root_ast_node])
88
88
 
89
- @graph_model.edge(_type='edge',
89
+ @graph_model.edge(type_='edge',
90
90
  source=source,
91
91
  target=target,
92
92
  label=operator.itemgetter('type'))
@@ -0,0 +1,53 @@
1
+ import itertools
2
+ import operator
3
+ import sys
4
+
5
+ from pygments import lex
6
+ from pygments.lexers import guess_lexer_for_filename
7
+
8
+ import graphinate
9
+
10
+
11
+ def load_file(file_path):
12
+ with open(file_path) as file:
13
+ return file.read()
14
+
15
+
16
+ def tokenize_file(file_path):
17
+ content = load_file(file_path)
18
+ lexer = guess_lexer_for_filename(file_path, content)
19
+ return lex(content, lexer)
20
+
21
+
22
+ def token_graph_model(file_path):
23
+ graph_model = graphinate.model(name="Token Graph")
24
+
25
+ def token_type(v):
26
+ return str(v[0]).replace('.', '_')
27
+
28
+ def token_key(v):
29
+ return f"{v[0]}-{v[1]}"
30
+
31
+ @graph_model.node(token_type, key=token_key)
32
+ def token():
33
+ yield from tokenize_file(file_path)
34
+
35
+ @graph_model.edge(source=operator.itemgetter(0), target=operator.itemgetter(1))
36
+ def edge():
37
+ yield from itertools.pairwise(token_key(t) for t in tokenize_file(file_path))
38
+
39
+ return graph_model
40
+
41
+
42
+ if __name__ == '__main__':
43
+ if len(sys.argv) != 2:
44
+ print("Usage: python tokens.py <file_path>")
45
+ sys.exit(1)
46
+
47
+ file_path = sys.argv[1]
48
+ token_model = token_graph_model(file_path)
49
+ graphinate.materialize(
50
+ token_model,
51
+ builder=graphinate.builders.GraphQLBuilder,
52
+ builder_output_handler=graphinate.graphql
53
+ )
@@ -18,7 +18,7 @@ def repo_graph_model(): # noqa: C901
18
18
 
19
19
  graph_model = graphinate.model(name='GitHub Repository Graph')
20
20
 
21
- @graph_model.edge
21
+ @graph_model.edge()
22
22
  def github(user_id: Optional[str] = None,
23
23
  repository_id: Optional[str] = None,
24
24
  commit_id: Optional[str] = None,
@@ -56,7 +56,7 @@ def repo_graph_model(): # noqa: C901
56
56
  label=commit_label)
57
57
 
58
58
  file_node = graph_model.node(parent_type='commit',
59
- uniqueness=True,
59
+ unique=True,
60
60
  key=operator.attrgetter('filename'),
61
61
  value=operator.attrgetter('raw_data'),
62
62
  label=operator.itemgetter('filename'))
@@ -50,7 +50,7 @@ if __name__ == '__main__':
50
50
 
51
51
  graph_atlas = graphs.atlas()
52
52
 
53
- listbox_chooser = ListboxChooser('Choose Graph', graph_atlas)
53
+ listbox_chooser = ListboxChooser('Choose Graph/s', graph_atlas)
54
54
  choices = list(listbox_chooser.get_choices())
55
55
  model = model(choices)
56
56
 
@@ -0,0 +1,2 @@
1
+ graphinate
2
+ networkx
@@ -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)
@@ -0,0 +1,149 @@
1
+ import logging
2
+ import operator
3
+ import pathlib
4
+ from functools import reduce
5
+ from time import sleep
6
+
7
+ import diskcache
8
+ import musicbrainzngs
9
+
10
+ import graphinate
11
+
12
+ # logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def initialize_musicbrainz():
17
+ musicbrainzngs.set_useragent(
18
+ "MusicArtistGraph",
19
+ "0.1.0",
20
+ "https://github.com/erivlis/graphinate"
21
+ )
22
+
23
+
24
+ initialize_musicbrainz()
25
+
26
+
27
+ def cache_dir():
28
+ current_script_path = pathlib.Path(__file__).resolve()
29
+ parent_dir = current_script_path.parent
30
+ return (parent_dir / 'cache').as_posix()
31
+
32
+
33
+ def music_graph_model(name: str, max_depth: int = 0):
34
+ graph_model = graphinate.model(f"{name.capitalize()} Graph")
35
+
36
+ artists_cache = diskcache.Cache(directory=cache_dir(), eviction_policy='none')
37
+
38
+ result = musicbrainzngs.search_artists(query=name, strict=True, artist=name)
39
+ sleep(1)
40
+ root_artist = result.get('artist-list', [])[0] if result else None
41
+
42
+ def artists(parent_artist, artist, depth):
43
+ logger.info(f"Current depth: {depth}")
44
+ artist_id = artist.get('id')
45
+ if artist_id not in artists_cache:
46
+ artists_cache[artist_id] = musicbrainzngs.get_artist_by_id(id=artist_id, includes=['artist-rels']).get(
47
+ 'artist')
48
+ sleep(0.1)
49
+
50
+ artist = artists_cache.get(artist_id)
51
+
52
+ yield parent_artist, artist
53
+
54
+ if depth < max_depth:
55
+ related_artist_ids = set()
56
+ for item in artist.get('artist-relation-list', []):
57
+ related_artist = item.get('artist')
58
+ related_artist_id = related_artist.get('id')
59
+ if related_artist_id not in related_artist_ids:
60
+ related_artist_ids.add(related_artist_id)
61
+ yield from artists(artist, related_artist, depth + 1)
62
+
63
+ def artist_type(value):
64
+ return value.get('type', '_UNKNOWN_')
65
+
66
+ @graph_model.node(artist_type,
67
+ key=operator.itemgetter('id'),
68
+ label=operator.itemgetter('name'),
69
+ multiplicity=graphinate.Multiplicity.FIRST)
70
+ def node():
71
+ yielded = set()
72
+ for a, b in artists(None, root_artist, 0):
73
+ if a and ((a_id := a.get('id')) not in yielded):
74
+ yielded.add(a_id)
75
+ yield a
76
+ if b and ((b_id := b.get('id')) not in yielded):
77
+ yielded.add(b_id)
78
+ yield b
79
+
80
+ @graph_model.edge()
81
+ def edge():
82
+ for a, b in artists(None, root_artist, 0):
83
+ if a:
84
+ yield {'source': a.get('id'), 'target': b.get('id')}
85
+
86
+ return graph_model
87
+
88
+
89
+ if __name__ == '__main__':
90
+ from gui import ListboxChooser
91
+
92
+ artist_names = [
93
+ 'Alice in Chains',
94
+ 'Beatles',
95
+ 'Caravan',
96
+ 'Charles Mingus',
97
+ 'Dave Brubeck',
98
+ 'Dave Douglas',
99
+ 'David Bowie',
100
+ 'Deep Purple',
101
+ 'Dire Straits',
102
+ 'Emerson, Lake & Palmer',
103
+ 'Foo Fighters',
104
+ 'Frank Zappa',
105
+ 'Genesis',
106
+ 'Gentle Giant',
107
+ 'Herbie Hancock',
108
+ 'Jethro Tull',
109
+ 'John Coltrane',
110
+ 'John Scofield',
111
+ 'John Zorn',
112
+ 'Ken Vandermark',
113
+ 'King Crimson',
114
+ 'Led Zeppelin',
115
+ 'Mahavishnu Orchestra',
116
+ 'Miles Davis',
117
+ 'Nirvana',
118
+ 'Ornette Coleman',
119
+ 'Paul McCartney',
120
+ 'Pearl Jam',
121
+ 'Pink Floyd',
122
+ 'Police',
123
+ 'Porcupine Tree',
124
+ 'Radiohead',
125
+ 'Red Hot Chili Peppers',
126
+ 'Return to Forever',
127
+ 'Rush',
128
+ 'Smashing Pumpkins',
129
+ 'Soft Machine',
130
+ 'Soundgarden',
131
+ 'Stone Temple Pilots',
132
+ 'System of a Down',
133
+ 'Thelonious Monk',
134
+ 'Weather Report',
135
+ 'Wings',
136
+ 'Yes',
137
+ ]
138
+
139
+ listbox_chooser = ListboxChooser('Choose Artist/s', {name: name for name in artist_names})
140
+
141
+ models = (music_graph_model(a, 2) for _, a in listbox_chooser.get_choices())
142
+
143
+ model = reduce(operator.add, models)
144
+
145
+ graphinate.materialize(
146
+ model,
147
+ builder=graphinate.builders.GraphQLBuilder,
148
+ builder_output_handler=graphinate.graphql
149
+ )
@@ -0,0 +1,3 @@
1
+ diskcache
2
+ graphinate
3
+ musicbrainzngs