Graphinate 0.5.1__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 (122) hide show
  1. {graphinate-0.5.1 → graphinate-0.7.0}/.github/workflows/publish.yml +1 -1
  2. {graphinate-0.5.1 → graphinate-0.7.0}/.github/workflows/test.yml +1 -1
  3. {graphinate-0.5.1 → graphinate-0.7.0}/.gitignore +2 -1
  4. {graphinate-0.5.1 → graphinate-0.7.0}/PKG-INFO +4 -1
  5. {graphinate-0.5.1 → graphinate-0.7.0}/README.md +2 -0
  6. graphinate-0.7.0/docs/examples/social.md +15 -0
  7. {graphinate-0.5.1 → graphinate-0.7.0}/docs/intro.md +14 -10
  8. {graphinate-0.5.1 → graphinate-0.7.0}/docs/usage/cli.md +8 -0
  9. graphinate-0.7.0/examples/code/html_dom.py +61 -0
  10. {graphinate-0.5.1 → graphinate-0.7.0}/examples/code/python_ast.py +3 -3
  11. graphinate-0.7.0/examples/code/tokens.py +53 -0
  12. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/repositories.py +2 -2
  13. {graphinate-0.5.1 → graphinate-0.7.0}/examples/math/graph_atlas.py +3 -7
  14. graphinate-0.7.0/examples/math/requirements.txt +2 -0
  15. graphinate-0.7.0/examples/social/cache/13/dd/73ce25face7beb30b69b64feeb77.val +0 -0
  16. graphinate-0.7.0/examples/social/cache/21/9e/00846f323987ba16cfbe0127d8eb.val +0 -0
  17. graphinate-0.7.0/examples/social/cache/70/b6/2aefb0269adce7fedf877fa0d267.val +0 -0
  18. graphinate-0.7.0/examples/social/cache/87/f5/ec1739bc369e84c3fcb302bf532a.val +0 -0
  19. graphinate-0.7.0/examples/social/cache/ba/fe/3aca7b2c38abff60e7ce5eb486a8.val +0 -0
  20. graphinate-0.7.0/examples/social/cache/c7/9e/ce82b0288020b7152779df09bd73.val +0 -0
  21. graphinate-0.7.0/examples/social/cache/cache.db +0 -0
  22. graphinate-0.7.0/examples/social/cache/d2/53/3b88f2fc162561cfdbbe9abc352a.val +0 -0
  23. graphinate-0.7.0/examples/social/cache/e2/d5/5d079f200eabf9b625b0473f6fbe.val +0 -0
  24. graphinate-0.7.0/examples/social/gui.py +108 -0
  25. graphinate-0.7.0/examples/social/music_artists.py +149 -0
  26. graphinate-0.7.0/examples/social/requirements.txt +3 -0
  27. graphinate-0.7.0/examples/system/.ignore +796 -0
  28. graphinate-0.7.0/examples/system/files.py +81 -0
  29. {graphinate-0.5.1 → graphinate-0.7.0}/examples/system/requirements.txt +1 -0
  30. {graphinate-0.5.1 → graphinate-0.7.0}/mkdocs.yml +6 -2
  31. {graphinate-0.5.1 → graphinate-0.7.0}/pyproject.toml +11 -10
  32. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/__init__.py +2 -1
  33. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/builders.py +63 -104
  34. graphinate-0.7.0/src/graphinate/constants.py +2 -0
  35. graphinate-0.7.0/src/graphinate/converters.py +91 -0
  36. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/modeling.py +53 -32
  37. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/__init__.py +12 -4
  38. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/rapidoc/index.html +3 -2
  39. graphinate-0.7.0/src/graphinate/server/web/viewer/index.html +661 -0
  40. graphinate-0.5.1/src/graphinate/tools/__init__.py → graphinate-0.7.0/src/graphinate/tools.py +3 -1
  41. {graphinate-0.5.1 → graphinate-0.7.0}/tests/conftest.py +6 -5
  42. {graphinate-0.5.1 → graphinate-0.7.0}/tests/graphinate/test_builders.py +26 -64
  43. graphinate-0.7.0/tests/graphinate/test_converters.py +60 -0
  44. {graphinate-0.5.1 → graphinate-0.7.0}/tests/graphinate/test_modeling.py +4 -3
  45. graphinate-0.5.1/examples/math/requirements.txt +0 -1
  46. graphinate-0.5.1/src/graphinate/server/web/viewer/index.html +0 -162
  47. graphinate-0.5.1/src/graphinate/tools/converters.py +0 -28
  48. graphinate-0.5.1/src/graphinate/tools/mutators.py +0 -153
  49. graphinate-0.5.1/tests/graphinate/test_tools.py +0 -65
  50. {graphinate-0.5.1 → graphinate-0.7.0}/.coveragerc +0 -0
  51. {graphinate-0.5.1 → graphinate-0.7.0}/.github/dependabot.yml +0 -0
  52. {graphinate-0.5.1 → graphinate-0.7.0}/.github/workflows/codeql.yml +0 -0
  53. {graphinate-0.5.1 → graphinate-0.7.0}/.github/workflows/publish-docs.yaml +0 -0
  54. {graphinate-0.5.1 → graphinate-0.7.0}/.github/workflows/test-beta.yml +0 -0
  55. {graphinate-0.5.1 → graphinate-0.7.0}/.sonarcloud.properties +0 -0
  56. {graphinate-0.5.1 → graphinate-0.7.0}/LICENSE +0 -0
  57. {graphinate-0.5.1 → graphinate-0.7.0}/STATS.md +0 -0
  58. {graphinate-0.5.1 → graphinate-0.7.0}/docs/acknowledge.md +0 -0
  59. {graphinate-0.5.1 → graphinate-0.7.0}/docs/assets/badge/v0.json +0 -0
  60. {graphinate-0.5.1 → graphinate-0.7.0}/docs/assets/images/logo-128.png +0 -0
  61. {graphinate-0.5.1 → graphinate-0.7.0}/docs/assets/images/network_graph.png +0 -0
  62. {graphinate-0.5.1 → graphinate-0.7.0}/docs/assets/stylesheets/extra.css +0 -0
  63. {graphinate-0.5.1 → graphinate-0.7.0}/docs/dev.md +0 -0
  64. {graphinate-0.5.1 → graphinate-0.7.0}/docs/examples/code.md +0 -0
  65. {graphinate-0.5.1 → graphinate-0.7.0}/docs/examples/github.md +0 -0
  66. {graphinate-0.5.1 → graphinate-0.7.0}/docs/examples/math.md +0 -0
  67. {graphinate-0.5.1 → graphinate-0.7.0}/docs/examples/system.md +0 -0
  68. {graphinate-0.5.1 → graphinate-0.7.0}/docs/examples/web.md +0 -0
  69. {graphinate-0.5.1 → graphinate-0.7.0}/docs/gen_ref_pages.py +0 -0
  70. {graphinate-0.5.1 → graphinate-0.7.0}/docs/index.md +0 -0
  71. {graphinate-0.5.1 → graphinate-0.7.0}/docs/start.md +0 -0
  72. {graphinate-0.5.1 → graphinate-0.7.0}/docs/usage/lib.md +0 -0
  73. {graphinate-0.5.1 → graphinate-0.7.0}/examples/code/python_dependencies.py +0 -0
  74. {graphinate-0.5.1 → graphinate-0.7.0}/examples/code/requirements.txt +0 -0
  75. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/_client.py +0 -0
  76. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/commits_visibilty_graph.py +0 -0
  77. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/followers.graphql +0 -0
  78. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/followers.py +0 -0
  79. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/graphql.config.yml +0 -0
  80. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/repositories.graphql +0 -0
  81. {graphinate-0.5.1 → graphinate-0.7.0}/examples/github/requirements.txt +0 -0
  82. {graphinate-0.5.1 → graphinate-0.7.0}/examples/math/graphs.py +0 -0
  83. {graphinate-0.5.1 → graphinate-0.7.0}/examples/math/gui.py +0 -0
  84. {graphinate-0.5.1 → graphinate-0.7.0}/examples/math/polygonal_graph.py +0 -0
  85. {graphinate-0.5.1 → graphinate-0.7.0}/examples/system/processes.py +0 -0
  86. {graphinate-0.5.1 → graphinate-0.7.0}/examples/web/page_links.py +0 -0
  87. {graphinate-0.5.1 → graphinate-0.7.0}/examples/web/requirements.txt +0 -0
  88. {graphinate-0.5.1 → graphinate-0.7.0}/playground/ethernet/traceroute.py +0 -0
  89. {graphinate-0.5.1 → graphinate-0.7.0}/playground/genric_graph.graphql +0 -0
  90. {graphinate-0.5.1 → graphinate-0.7.0}/playground/graphql.config.yml +0 -0
  91. {graphinate-0.5.1 → graphinate-0.7.0}/playground/house_of_graphs.py +0 -0
  92. {graphinate-0.5.1 → graphinate-0.7.0}/playground/social/albums.json +0 -0
  93. {graphinate-0.5.1 → graphinate-0.7.0}/playground/social/musicisians.py +0 -0
  94. {graphinate-0.5.1 → graphinate-0.7.0}/playground/text/nlp_graph.py +0 -0
  95. {graphinate-0.5.1 → graphinate-0.7.0}/playground/text/requirements.txt +0 -0
  96. {graphinate-0.5.1 → graphinate-0.7.0}/playground/time_series/requirements.txt +0 -0
  97. {graphinate-0.5.1 → graphinate-0.7.0}/playground/time_series/visibility_graph.py +0 -0
  98. {graphinate-0.5.1 → graphinate-0.7.0}/sonar-project.properties +0 -0
  99. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/__main__.py +0 -0
  100. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/cli.py +0 -0
  101. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/color.py +0 -0
  102. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/materializers/__init__.py +0 -0
  103. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/materializers/matplotlib.py +0 -0
  104. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/starlette/__init__.py +0 -0
  105. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/starlette/views.py +0 -0
  106. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/__init__.py +0 -0
  107. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/elements/__init__.py +0 -0
  108. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/elements/index.html +0 -0
  109. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/graphiql/__init__.py +0 -0
  110. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/graphiql/index.html +0 -0
  111. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/rapidoc/__init__.py +0 -0
  112. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/static/images/logo-128.png +0 -0
  113. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/static/images/logo.svg +0 -0
  114. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/static/images/network_graph.png +0 -0
  115. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/viewer/__init__.py +0 -0
  116. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/voyager/__init__.py +0 -0
  117. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/server/web/voyager/index.html +0 -0
  118. {graphinate-0.5.1 → graphinate-0.7.0}/src/graphinate/typing.py +0 -0
  119. {graphinate-0.5.1 → graphinate-0.7.0}/tests/graphinate/test_cli.py +0 -0
  120. {graphinate-0.5.1 → graphinate-0.7.0}/tests/graphinate/test_color.py +0 -0
  121. {graphinate-0.5.1 → graphinate-0.7.0}/tests/graphinate/test_materializers.py +0 -0
  122. {graphinate-0.5.1 → graphinate-0.7.0}/tests/graphinate/test_server.py +0 -0
@@ -28,7 +28,7 @@ jobs:
28
28
  - name: Build package
29
29
  run: python -m build
30
30
  - name: Publish package
31
- uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
31
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
32
32
  with:
33
33
  user: __token__
34
34
  password: ${{ secrets.PYPI_API_TOKEN }}
@@ -40,7 +40,7 @@ jobs:
40
40
  python -m pip install faker pytest pytest-asyncio pytest-cov pytest-randomly pytest-xdist starlette-prometheus uvicorn[standard]
41
41
  pytest tests --cov=src --cov-branch --cov-report=xml --junitxml=test_results.xml -n auto
42
42
  - name: Upload coverage reports to Codecov
43
- uses: codecov/codecov-action@v4
43
+ uses: codecov/codecov-action@v5
44
44
  with:
45
45
  token: ${{ secrets.CODECOV_TOKEN }}
46
46
  - name: Run codacy-coverage-reporter
@@ -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.5.1
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]
@@ -221,6 +222,8 @@ Options:
221
222
 
222
223
  #### Server
223
224
 
225
+ > TIP: requires the `server` extra to be installed. e.g., `pip install graphinate[server]`
226
+
224
227
  ```text
225
228
  Usage: python -m graphinate server [OPTIONS]
226
229
 
@@ -179,6 +179,8 @@ Options:
179
179
 
180
180
  #### Server
181
181
 
182
+ > TIP: requires the `server` extra to be installed. e.g., `pip install graphinate[server]`
183
+
182
184
  ```text
183
185
  Usage: python -m graphinate server [OPTIONS]
184
186
 
@@ -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
+ ```
@@ -15,21 +15,25 @@ In addition, there are several modes of output to enable examination of the Grap
15
15
 
16
16
  ### What is a Graph?
17
17
 
18
- “In a mathematician's terminology, a graph is a collection of points and lines connecting some (possibly empty) subset
19
- of them.
20
- The points of a graph are most commonly known as graph *vertices*, but may also be called *nodes* or *points*.
21
- Similarly, the lines connecting the vertices of a graph are most commonly known as graph *edges*, but may also
22
- be called *arcs* or *lines*.”
18
+ !!! quote
23
19
 
24
- &mdash; [https://mathworld.wolfram.com/Graph.html](https://mathworld.wolfram.com/Graph.html)
20
+ “In a mathematician's terminology, a graph is a collection of points and lines connecting some (possibly empty) subset
21
+ of them.
22
+ The points of a graph are most commonly known as graph *vertices*, but may also be called *nodes* or *points*.
23
+ Similarly, the lines connecting the vertices of a graph are most commonly known as graph *edges*, but may also
24
+ be called *arcs* or *lines*.”
25
+
26
+ &mdash; [https://mathworld.wolfram.com/Graph.html](https://mathworld.wolfram.com/Graph.html)
25
27
 
26
28
  ### What is Data?
27
29
 
28
- “...data is a collection of discrete or continuous values that convey information, describing the quantity, quality,
29
- fact, statistics, other basic units of meaning, or simply sequences of symbols that may be further interpreted
30
- formally.”
30
+ !!! quote
31
31
 
32
- &mdash; [https://en.wikipedia.org/wiki/Data](https://en.wikipedia.org/wiki/Data)
32
+ “...data is a collection of discrete or continuous values that convey information, describing the quantity, quality,
33
+ fact, statistics, other basic units of meaning, or simply sequences of symbols that may be further interpreted
34
+ formally.”
35
+
36
+ &mdash; [https://en.wikipedia.org/wiki/Data](https://en.wikipedia.org/wiki/Data)
33
37
 
34
38
  ## How?
35
39
 
@@ -28,6 +28,14 @@ Options:
28
28
 
29
29
  ### Server
30
30
 
31
+ !!! tip
32
+
33
+ requires the `server` extra to be installed.
34
+
35
+ ```shell
36
+ pip install graphinate[server]
37
+ ```
38
+
31
39
  ```console
32
40
  Usage: python -m graphinate server [OPTIONS]
33
41
 
@@ -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'))
@@ -38,13 +38,9 @@ def model(items: list[tuple[str, nx.Graph]]) -> graphinate.GraphModel:
38
38
  def nodes():
39
39
  yield from g.nodes(data='type')
40
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))
41
+ @graph_model.edge(operator.itemgetter('type'))
46
42
  def edge():
47
- yield from g.edges.data('type')
43
+ yield from ({'source': e[0], 'target': e[1], **e[2]} for e in g.edges.data())
48
44
 
49
45
  return graph_model
50
46
 
@@ -54,7 +50,7 @@ if __name__ == '__main__':
54
50
 
55
51
  graph_atlas = graphs.atlas()
56
52
 
57
- listbox_chooser = ListboxChooser('Choose Graph', graph_atlas)
53
+ listbox_chooser = ListboxChooser('Choose Graph/s', graph_atlas)
58
54
  choices = list(listbox_chooser.get_choices())
59
55
  model = model(choices)
60
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