Graphinate 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphinate/__init__.py +18 -0
- graphinate/__main__.py +4 -0
- graphinate/builders/__init__.py +55 -0
- graphinate/builders/_builder.py +58 -0
- graphinate/builders/_d3.py +61 -0
- graphinate/builders/_graphql.py +521 -0
- graphinate/builders/_mermaid.py +45 -0
- graphinate/builders/_networkx.py +227 -0
- graphinate/cli.py +123 -0
- graphinate/color.py +100 -0
- graphinate/constants.py +4 -0
- graphinate/converters.py +94 -0
- graphinate/enums.py +44 -0
- graphinate/modeling.py +337 -0
- graphinate/renderers/__init__.py +5 -0
- graphinate/renderers/graphql.py +111 -0
- graphinate/renderers/matplotlib.py +82 -0
- graphinate/server/__init__.py +0 -0
- graphinate/server/starlette/__init__.py +31 -0
- graphinate/server/starlette/views.py +17 -0
- graphinate/server/web/__init__.py +25 -0
- graphinate/server/web/elements/index.html +23 -0
- graphinate/server/web/graphiql/index.html +160 -0
- graphinate/server/web/rapidoc/index.html +17 -0
- graphinate/server/web/static/images/logo-128.png +0 -0
- graphinate/server/web/static/images/logo.svg +50 -0
- graphinate/server/web/static/images/network_graph.png +0 -0
- graphinate/server/web/viewer/index.html +719 -0
- graphinate/server/web/voyager/index.html +55 -0
- graphinate/tools.py +7 -0
- graphinate/typing.py +83 -0
- graphinate-0.12.0.dist-info/METADATA +284 -0
- graphinate-0.12.0.dist-info/RECORD +36 -0
- graphinate-0.12.0.dist-info/WHEEL +4 -0
- graphinate-0.12.0.dist-info/entry_points.txt +2 -0
- graphinate-0.12.0.dist-info/licenses/LICENSE +165 -0
graphinate/modeling.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import itertools
|
|
3
|
+
from collections import defaultdict, namedtuple
|
|
4
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from typing import Any, Union
|
|
8
|
+
|
|
9
|
+
from .typing import Edge, Element, Extractor, Items, Node, NodeTypeAbsoluteId, UniverseNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GraphModelError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def element(element_type: str | None, field_names: Iterable[str] | None = None) -> Callable[[], Element]:
|
|
17
|
+
"""Graph Element Supplier Callable
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
element_type:
|
|
21
|
+
field_names:
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Element Supplier Callable
|
|
25
|
+
"""
|
|
26
|
+
return namedtuple(element_type, field_names) if element_type and field_names else tuple
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def extractor(obj: Any, key: Extractor | None = None) -> str | None:
|
|
30
|
+
"""Extract data item from Element
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
obj:
|
|
34
|
+
key:
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Element data item
|
|
38
|
+
"""
|
|
39
|
+
if key is None:
|
|
40
|
+
return obj
|
|
41
|
+
|
|
42
|
+
if callable(key):
|
|
43
|
+
return key(obj)
|
|
44
|
+
|
|
45
|
+
if isinstance(obj, Mapping) and isinstance(key, str):
|
|
46
|
+
return obj.get(key, key)
|
|
47
|
+
|
|
48
|
+
return key
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def elements(iterable: Iterable[Any],
|
|
52
|
+
element_type: Extractor | None = None,
|
|
53
|
+
**getters: Extractor) -> Iterable[Element]:
|
|
54
|
+
"""Abstract Generator of Graph elements (nodes or edges)
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
iterable: source of payload
|
|
58
|
+
element_type: Optional[Extractor] source of type of the element. Defaults to Element Type name.
|
|
59
|
+
getters: Extractor node field sources
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Iterable of Elements.
|
|
63
|
+
"""
|
|
64
|
+
for item in iterable:
|
|
65
|
+
_type = element_type(item) if element_type and callable(element_type) else element_type
|
|
66
|
+
if not _type.isidentifier():
|
|
67
|
+
raise ValueError(f"Invalid Type: {_type}. Must be a valid Python identifier.")
|
|
68
|
+
|
|
69
|
+
create_element = element(_type, getters.keys())
|
|
70
|
+
kwargs = {k: extractor(item, v) for k, v in getters.items()}
|
|
71
|
+
yield create_element(**kwargs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Multiplicity(Enum):
|
|
75
|
+
ADD = auto()
|
|
76
|
+
ALL = auto()
|
|
77
|
+
FIRST = auto()
|
|
78
|
+
LAST = auto()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class NodeModel:
|
|
83
|
+
"""Represents a Node Model
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
type: the type of the Node.
|
|
87
|
+
parent_type: the type of the node's parent. Defaults to UniverseNode.
|
|
88
|
+
parameters: parameters of the Node. Defaults to None.
|
|
89
|
+
label: label source. Defaults to None.
|
|
90
|
+
uniqueness: is the Node universally unique. Defaults to True.
|
|
91
|
+
multiplicity: Multiplicity of the Node. Defaults to ALL.
|
|
92
|
+
generator: Nodes generator method. Defaults to None.
|
|
93
|
+
|
|
94
|
+
Properties:
|
|
95
|
+
absolute_id: return the NodeModel absolute_id.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
type: str
|
|
99
|
+
parent_type: str | UniverseNode | None = UniverseNode
|
|
100
|
+
parameters: set[str] | None = None
|
|
101
|
+
label: Callable[[Any], str | None] = None
|
|
102
|
+
uniqueness: bool = True
|
|
103
|
+
multiplicity: Multiplicity = Multiplicity.ALL
|
|
104
|
+
generator: Callable[[], Iterable[Node]] | None = None
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def absolute_id(self) -> NodeTypeAbsoluteId:
|
|
108
|
+
return self.parent_type, self.type
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class GraphModel:
|
|
112
|
+
"""A Graph Model
|
|
113
|
+
|
|
114
|
+
Used to declaratively register Edge and/or Node data supplier functions by using
|
|
115
|
+
decorators.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name: the archetype name for Graphs generated based on the GraphModel.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, name: str):
|
|
122
|
+
self.name: str = name
|
|
123
|
+
self._node_models: dict[NodeTypeAbsoluteId, list[NodeModel]] = defaultdict(list)
|
|
124
|
+
self._node_children: dict[str, list[NodeModel]] = defaultdict(list)
|
|
125
|
+
self._edge_generators: dict[str, list[Callable[[], Iterable[Edge]]]] = defaultdict(list)
|
|
126
|
+
self._networkx_graph = None
|
|
127
|
+
|
|
128
|
+
def __add__(self, other: 'GraphModel') -> 'GraphModel':
|
|
129
|
+
graph_model = GraphModel(name=f"{self.name} + {other.name}")
|
|
130
|
+
for m in (self, other):
|
|
131
|
+
for k, v in m._node_models.items():
|
|
132
|
+
graph_model._node_models[k].extend(v)
|
|
133
|
+
|
|
134
|
+
for k, v in m._node_children.items():
|
|
135
|
+
graph_model._node_children[k].extend(v)
|
|
136
|
+
|
|
137
|
+
for k, v in m._edge_generators.items():
|
|
138
|
+
graph_model._edge_generators[k].extend(v)
|
|
139
|
+
|
|
140
|
+
return graph_model
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def node_models(self) -> dict[NodeTypeAbsoluteId, list[NodeModel]]:
|
|
144
|
+
"""
|
|
145
|
+
Returns:
|
|
146
|
+
NodeModel for Node Types. Key values are NodeTypeAbsoluteId.
|
|
147
|
+
"""
|
|
148
|
+
return self._node_models
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def edge_generators(self):
|
|
152
|
+
"""
|
|
153
|
+
Returns:
|
|
154
|
+
Edge generator functions for Edge Types
|
|
155
|
+
"""
|
|
156
|
+
return self._edge_generators
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def node_types(self) -> set[str]:
|
|
160
|
+
"""
|
|
161
|
+
Returns:
|
|
162
|
+
Node Types
|
|
163
|
+
"""
|
|
164
|
+
return {v.type for v in itertools.chain.from_iterable(self._node_models.values())}
|
|
165
|
+
|
|
166
|
+
def node_children_types(self, _type: str = UniverseNode) -> dict[str, list[str]]:
|
|
167
|
+
"""Children Node Types for given input Node Type
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
_type: Node Type. Default value is UNIVERSE_NODE.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of children Node Types.
|
|
174
|
+
"""
|
|
175
|
+
return {k: v for k, v in self._node_children.items() if k == _type}
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _validate_type(node_type: str):
|
|
179
|
+
if not callable(node_type) and not node_type.isidentifier():
|
|
180
|
+
raise ValueError(f"Invalid Type: {node_type}. Must be a valid Python identifier.")
|
|
181
|
+
|
|
182
|
+
def _validate_node_parameters(self, parameters: list[str]):
|
|
183
|
+
node_types = self.node_types
|
|
184
|
+
if not all(p.endswith('_id') and p == p.lower() and p[:-3] in node_types for p in parameters):
|
|
185
|
+
msg = ("Illegal Arguments. Argument should conform to the following rules: "
|
|
186
|
+
"1) lowercase "
|
|
187
|
+
"2) end with '_id' "
|
|
188
|
+
"3) start with value that exists as registered node type")
|
|
189
|
+
|
|
190
|
+
raise GraphModelError(msg)
|
|
191
|
+
|
|
192
|
+
def node(self,
|
|
193
|
+
type_: Extractor | None = None,
|
|
194
|
+
parent_type: str | None = UniverseNode,
|
|
195
|
+
key: Extractor | None = None,
|
|
196
|
+
value: Extractor | None = None,
|
|
197
|
+
label: Extractor | None = None,
|
|
198
|
+
unique: bool = True,
|
|
199
|
+
multiplicity: Multiplicity = Multiplicity.ALL) -> Callable[[Items], None]:
|
|
200
|
+
"""Decorator to Register a Generator of node payloads as a source for Graph Nodes.
|
|
201
|
+
It creates a NodeModel object.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
type_: Optional source for the Node Type. Defaults to use Generator function
|
|
205
|
+
name as the Node Type.
|
|
206
|
+
parent_type: Optional parent Node Type. Defaults to UNIVERSE_NODE
|
|
207
|
+
|
|
208
|
+
key: Optional source for Node IDs. Defaults to use the complete Node payload
|
|
209
|
+
as Node ID.
|
|
210
|
+
value: Optional source for Node value field. Defaults to use the complete
|
|
211
|
+
Node payload as Node ID.
|
|
212
|
+
label: Optional source for Node label field. Defaults to use a 'str'
|
|
213
|
+
representation of the complete Node payload.
|
|
214
|
+
unique: is the Node universally unique. Defaults to True.
|
|
215
|
+
multiplicity: Multiplicity of the Node. Defaults to ALL.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
None
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def register_node(f: Items):
|
|
222
|
+
node_type = type_ or f.__name__
|
|
223
|
+
self._validate_type(node_type)
|
|
224
|
+
|
|
225
|
+
model_type = f.__name__ if callable(node_type) else node_type
|
|
226
|
+
|
|
227
|
+
def node_generator(**kwargs: Any) -> Iterable[Node]:
|
|
228
|
+
yield from elements(f(**kwargs), node_type, key=key, value=value)
|
|
229
|
+
|
|
230
|
+
parameters = inspect.getfullargspec(f).args
|
|
231
|
+
node_model = NodeModel(type=model_type,
|
|
232
|
+
parent_type=parent_type,
|
|
233
|
+
parameters=set(parameters),
|
|
234
|
+
label=label,
|
|
235
|
+
uniqueness=unique,
|
|
236
|
+
multiplicity=multiplicity,
|
|
237
|
+
generator=node_generator)
|
|
238
|
+
self._node_models[node_model.absolute_id].append(node_model)
|
|
239
|
+
self._node_children[parent_type].append(model_type)
|
|
240
|
+
|
|
241
|
+
self._validate_node_parameters(parameters)
|
|
242
|
+
|
|
243
|
+
return register_node
|
|
244
|
+
|
|
245
|
+
def edge(self,
|
|
246
|
+
type_: Extractor | None = None,
|
|
247
|
+
source: Extractor = 'source',
|
|
248
|
+
target: Extractor = 'target',
|
|
249
|
+
label: Extractor | None = str,
|
|
250
|
+
value: Extractor | None = None,
|
|
251
|
+
weight: Union[float, Callable[[Any], float]] = 1.0,
|
|
252
|
+
) -> Callable[[Items], None]:
|
|
253
|
+
"""Decorator to Register a generator of edge payloads as a source of Graph Edges.
|
|
254
|
+
It creates an Edge generator function.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
type_: Optional source for the Edge Type. Defaults to use Generator function
|
|
258
|
+
name as the Edge Type.
|
|
259
|
+
source: Source for edge source Node ID.
|
|
260
|
+
target: Source for edge target Node ID.
|
|
261
|
+
label: Source for edge label.
|
|
262
|
+
value: Source for edge value.
|
|
263
|
+
weight: Source for edge weight.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
None.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def register_edge(f: Items):
|
|
270
|
+
edge_type = type_ or f.__name__
|
|
271
|
+
self._validate_type(edge_type)
|
|
272
|
+
|
|
273
|
+
model_type = f.__name__ if callable(edge_type) else edge_type
|
|
274
|
+
|
|
275
|
+
getters = {
|
|
276
|
+
'source': source,
|
|
277
|
+
'target': target,
|
|
278
|
+
'label': label,
|
|
279
|
+
'type': edge_type,
|
|
280
|
+
'value': value,
|
|
281
|
+
'weight': weight
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
def edge_generator(**kwargs: Any) -> Iterable[Edge]:
|
|
285
|
+
yield from elements(f(**kwargs), edge_type, **getters)
|
|
286
|
+
|
|
287
|
+
self._edge_generators[model_type].append(edge_generator)
|
|
288
|
+
|
|
289
|
+
return register_edge
|
|
290
|
+
|
|
291
|
+
def rectify(self, _type: Extractor | None = None,
|
|
292
|
+
parent_type: str | None = UniverseNode,
|
|
293
|
+
key: Extractor | None = None,
|
|
294
|
+
value: Extractor | None = None,
|
|
295
|
+
label: Extractor | None = None):
|
|
296
|
+
"""
|
|
297
|
+
Rectify the model.
|
|
298
|
+
Add a default NodeModel in case of having just edge supplier/s and no node supplier/s.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
_type
|
|
302
|
+
parent_type
|
|
303
|
+
key
|
|
304
|
+
value
|
|
305
|
+
label
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
None
|
|
309
|
+
"""
|
|
310
|
+
if self._edge_generators and not self._node_models:
|
|
311
|
+
@self.node(
|
|
312
|
+
type_=_type or 'node',
|
|
313
|
+
parent_type=parent_type or 'node',
|
|
314
|
+
unique=True,
|
|
315
|
+
key=key,
|
|
316
|
+
value=value,
|
|
317
|
+
label=label or str
|
|
318
|
+
)
|
|
319
|
+
def node(): # pragma: no cover
|
|
320
|
+
return
|
|
321
|
+
yield
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def model(name: str):
|
|
325
|
+
"""
|
|
326
|
+
Create a graph model
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
name: model name
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
GraphModel
|
|
333
|
+
"""
|
|
334
|
+
return GraphModel(name=name)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
__all__ = ('GraphModel', 'Multiplicity', 'model')
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import webbrowser
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
from starlette.applications import Starlette
|
|
7
|
+
from starlette.requests import Request
|
|
8
|
+
from starlette.responses import RedirectResponse
|
|
9
|
+
from starlette.schemas import SchemaGenerator
|
|
10
|
+
from starlette.types import ASGIApp
|
|
11
|
+
from strawberry.asgi import GraphQL
|
|
12
|
+
|
|
13
|
+
from graphinate.server.starlette import routes
|
|
14
|
+
|
|
15
|
+
DEFAULT_PORT: int = 8072
|
|
16
|
+
|
|
17
|
+
GRAPHQL_ROUTE_PATH = "/graphql"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _openapi_schema(request: Request) -> ASGIApp:
|
|
21
|
+
"""
|
|
22
|
+
Generates an OpenAPI schema for the GraphQL API and other routes.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
request (Request): The HTTP request object.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
ASGIApp: An OpenAPI response containing the schema for the specified routes.
|
|
29
|
+
"""
|
|
30
|
+
schema_data = {
|
|
31
|
+
'openapi': '3.0.0',
|
|
32
|
+
'info': {'title': 'Graphinate API', 'version': '0.10.1'},
|
|
33
|
+
'paths': {
|
|
34
|
+
'/graphql': {'get': {'responses': {200: {'description': 'GraphQL'}}}},
|
|
35
|
+
'/graphiql': {'get': {'responses': {200: {'description': 'GraphiQL UI.'}}}},
|
|
36
|
+
'/metrics': {'get': {'responses': {200: {'description': 'Prometheus metrics.'}}}},
|
|
37
|
+
'/viewer': {'get': {'responses': {200: {'description': '3D Force-Directed Graph Viewer'}}}},
|
|
38
|
+
'/voyager': {'get': {'responses': {200: {'description': 'Voyager GraphQL Schema Viewer'}}}}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
schema = SchemaGenerator(schema_data)
|
|
43
|
+
return schema.OpenAPIResponse(request=request)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _graphql_app(graphql_schema: strawberry.Schema) -> strawberry.asgi.GraphQL:
|
|
47
|
+
"""
|
|
48
|
+
Creates a Strawberry GraphQL app with the provided schema.
|
|
49
|
+
Args:
|
|
50
|
+
graphql_schema:
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
strawberry.asgi.GraphQL: The GraphQL app configured with the provided schema.
|
|
54
|
+
"""
|
|
55
|
+
graphql_app = GraphQL(graphql_schema, graphql_ide='apollo-sandbox')
|
|
56
|
+
return graphql_app
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _starlette_app(graphql_app: strawberry.asgi.GraphQL | None = None,
|
|
60
|
+
port: int = DEFAULT_PORT,
|
|
61
|
+
**kwargs: Any) -> Starlette:
|
|
62
|
+
def open_url(endpoint):
|
|
63
|
+
webbrowser.open(f'http://localhost:{port}/{endpoint}')
|
|
64
|
+
|
|
65
|
+
@contextlib.asynccontextmanager
|
|
66
|
+
async def lifespan(app: Starlette): # pragma: no cover
|
|
67
|
+
if kwargs.get('browse'):
|
|
68
|
+
open_url('viewer')
|
|
69
|
+
yield
|
|
70
|
+
|
|
71
|
+
app = Starlette(
|
|
72
|
+
lifespan=lifespan,
|
|
73
|
+
routes=routes()
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
from starlette_prometheus import PrometheusMiddleware, metrics
|
|
77
|
+
app.add_middleware(PrometheusMiddleware)
|
|
78
|
+
app.add_route("/metrics", metrics)
|
|
79
|
+
|
|
80
|
+
if graphql_app:
|
|
81
|
+
app.add_route(GRAPHQL_ROUTE_PATH, graphql_app)
|
|
82
|
+
app.add_websocket_route(GRAPHQL_ROUTE_PATH, graphql_app)
|
|
83
|
+
app.add_route("/schema", route=_openapi_schema, include_in_schema=False)
|
|
84
|
+
app.add_route("/openapi.json", route=_openapi_schema, include_in_schema=False)
|
|
85
|
+
|
|
86
|
+
def redirect_to_viewer(request):
|
|
87
|
+
return RedirectResponse(url='/viewer')
|
|
88
|
+
|
|
89
|
+
app.add_route('/', redirect_to_viewer)
|
|
90
|
+
|
|
91
|
+
return app
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def server(graphql_schema: strawberry.Schema, port: int = DEFAULT_PORT, **kwargs: Any):
|
|
95
|
+
"""
|
|
96
|
+
Args:
|
|
97
|
+
graphql_schema: The Strawberry GraphQL schema.
|
|
98
|
+
port: The port number to run the server on. Defaults to 8072.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
graphql_app = _graphql_app(graphql_schema)
|
|
104
|
+
|
|
105
|
+
app = _starlette_app(graphql_app, port=port, **kwargs)
|
|
106
|
+
|
|
107
|
+
import uvicorn
|
|
108
|
+
uvicorn.run(app, host='0.0.0.0', port=port)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = ['server']
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from matplotlib import pyplot
|
|
5
|
+
|
|
6
|
+
from ..color import node_color_mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def draw(graph: nx.Graph,
|
|
10
|
+
with_node_labels: bool = True,
|
|
11
|
+
with_edge_labels: bool = False,
|
|
12
|
+
**kwargs: Any) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Draws the given networkx graph with optional node and edge labels.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
graph (nx.Graph): The input graph to be drawn.
|
|
18
|
+
with_node_labels (bool): Whether to display node labels. Default is True.
|
|
19
|
+
with_edge_labels (bool): Whether to display edge labels. Default is False.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
None
|
|
23
|
+
"""
|
|
24
|
+
pos = nx.planar_layout(graph) if nx.is_planar(graph) else None
|
|
25
|
+
pos = nx.spring_layout(graph, pos=pos) if pos else nx.spring_layout(graph)
|
|
26
|
+
|
|
27
|
+
draw_params = {}
|
|
28
|
+
if with_node_labels:
|
|
29
|
+
draw_params.update(
|
|
30
|
+
{
|
|
31
|
+
'with_labels': True,
|
|
32
|
+
'labels': nx.get_node_attributes(graph, 'label'),
|
|
33
|
+
'font_size': 6,
|
|
34
|
+
'font_color': 'blue',
|
|
35
|
+
# 'horizontalalignment':'left',
|
|
36
|
+
# 'verticalalignment':'bottom',
|
|
37
|
+
# 'bbox': {
|
|
38
|
+
# 'boxstyle': 'round',
|
|
39
|
+
# 'fc': (0.02, 0.02, 0.02),
|
|
40
|
+
# 'lw': 0,
|
|
41
|
+
# 'alpha': 0.15,
|
|
42
|
+
# 'path_effects': [patheffects.withStroke(linewidth=1, foreground="red")]
|
|
43
|
+
# }
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
node_color = list(node_color_mapping(graph).values())
|
|
48
|
+
nx.draw(graph, pos, node_color=node_color, **draw_params)
|
|
49
|
+
if with_edge_labels:
|
|
50
|
+
nx.draw_networkx_edge_labels(graph,
|
|
51
|
+
pos,
|
|
52
|
+
edge_labels=nx.get_edge_attributes(graph, 'label'),
|
|
53
|
+
font_color='red',
|
|
54
|
+
font_size=6)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plot(graph: nx.Graph,
|
|
58
|
+
with_node_labels: bool = True,
|
|
59
|
+
with_edge_labels: bool = False,
|
|
60
|
+
**kwargs: Any) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Plots the given networkx graph with optional node and edge labels.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
graph (nx.Graph): The input graph to be plotted.
|
|
66
|
+
with_node_labels (bool): Whether to display node labels. Default is True.
|
|
67
|
+
with_edge_labels (bool): Whether to display edge labels. Default is False.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
None
|
|
71
|
+
"""
|
|
72
|
+
draw(graph, with_node_labels, with_edge_labels, **kwargs)
|
|
73
|
+
|
|
74
|
+
ax = pyplot.gca()
|
|
75
|
+
ax.margins(0.10)
|
|
76
|
+
|
|
77
|
+
fig = pyplot.gcf()
|
|
78
|
+
fig.suptitle(graph.name)
|
|
79
|
+
fig.tight_layout()
|
|
80
|
+
|
|
81
|
+
# pyplot.axis("off")
|
|
82
|
+
pyplot.show()
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from starlette.routing import BaseRoute, Mount
|
|
5
|
+
from starlette.staticfiles import StaticFiles
|
|
6
|
+
|
|
7
|
+
from ..web import paths_mapping
|
|
8
|
+
from .views import favicon_route
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _mount_static_files(named_paths: Mapping[str, Path]) -> list[Mount]:
|
|
12
|
+
mounts = []
|
|
13
|
+
for name, path in named_paths.items():
|
|
14
|
+
if not name.startswith('__'):
|
|
15
|
+
index_file = path / 'index.html'
|
|
16
|
+
static_files = StaticFiles(directory=path, html=index_file.exists(), check_dir=True)
|
|
17
|
+
mount = Mount(path=f"/{name}", app=static_files, name=name)
|
|
18
|
+
mounts.append(mount)
|
|
19
|
+
return mounts
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def routes() -> Sequence[BaseRoute]:
|
|
23
|
+
route_list: list[BaseRoute] = [
|
|
24
|
+
*_mount_static_files(paths_mapping),
|
|
25
|
+
favicon_route()
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
return route_list
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ('routes',)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from starlette.responses import FileResponse
|
|
5
|
+
from starlette.routing import Route
|
|
6
|
+
|
|
7
|
+
from ..web import get_static_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def favicon(request: Request) -> FileResponse:
|
|
11
|
+
path = get_static_path('images/logo-128.png').absolute().as_posix()
|
|
12
|
+
return FileResponse(path)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@functools.cache
|
|
16
|
+
def favicon_route() -> Route:
|
|
17
|
+
return Route('/favicon.ico', endpoint=favicon, include_in_schema=False)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import pathlib
|
|
3
|
+
from collections.abc import Generator, Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def current_file() -> pathlib.Path | None:
|
|
8
|
+
"""Returns current file name"""
|
|
9
|
+
if (current_frame := inspect.currentframe()) and current_frame.f_back is not None:
|
|
10
|
+
return pathlib.Path(inspect.getfile(current_frame.f_back))
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _current_file_peers() -> Generator[tuple[str, pathlib.Path], Any, None]:
|
|
15
|
+
_current_file = current_file()
|
|
16
|
+
if _current_file is not None:
|
|
17
|
+
for p in _current_file.parent.iterdir():
|
|
18
|
+
yield p.name, p
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
paths_mapping: Mapping[str, pathlib.Path] = dict(_current_file_peers())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_static_path(relative_path: str) -> pathlib.Path:
|
|
25
|
+
return paths_mapping['static'] / relative_path
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
6
|
+
|
|
7
|
+
<title>Graphinate - OpanAPI Elements UI</title>
|
|
8
|
+
<!-- Embed elements Elements via Web Component -->
|
|
9
|
+
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"
|
|
10
|
+
integrity="sha384-yR4p7dGVb43Z+zDGOg8xyIj71nVRZvN/IpUbptwjAtxcq+IcVQ+mNMiL7ppakjKc"></script>
|
|
11
|
+
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css"
|
|
12
|
+
integrity="sha384-fgRmJCkCe8Qyv2rSIDjIvkm779dv+HZbSa/ptwSeufq8qJZo/xsi79N8Rb6xTIMO">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
|
|
16
|
+
<elements-api
|
|
17
|
+
apiDescriptionUrl="/schema"
|
|
18
|
+
router="hash"
|
|
19
|
+
layout="sidebar"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|