dbgraph 0.1.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.
- dbgraph/__init__.py +21 -0
- dbgraph/application.py +70 -0
- dbgraph/builder/__init__.py +0 -0
- dbgraph/builder/graph_builder.py +22 -0
- dbgraph/builder/sql_graph_builder.py +245 -0
- dbgraph/descriptor/__init__.py +0 -0
- dbgraph/descriptor/graph_descriptor.py +68 -0
- dbgraph/descriptor/langchain_graph_descriptor.py +74 -0
- dbgraph/descriptor/prompt_templates.py +80 -0
- dbgraph/entity/__init__.py +0 -0
- dbgraph/entity/aspect.py +153 -0
- dbgraph/entity/asset.py +26 -0
- dbgraph/entity/asset_type.py +13 -0
- dbgraph/entity/dbgraph.py +152 -0
- dbgraph/entity/link.py +37 -0
- dbgraph/entity/link_type.py +13 -0
- dbgraph/entity/rdbgraph.py +76 -0
- dbgraph/event/__init__.py +0 -0
- dbgraph/event/event.py +20 -0
- dbgraph/event/event_bus.py +61 -0
- dbgraph/event/event_handler.py +11 -0
- dbgraph/io/__init__.py +0 -0
- dbgraph/io/graph_loader.py +11 -0
- dbgraph/io/graph_writer.py +11 -0
- dbgraph/io/json_graph_loader.py +160 -0
- dbgraph/io/json_graph_writer.py +17 -0
- dbgraph/persistent/__init__.py +0 -0
- dbgraph/persistent/graph_persistent.py +98 -0
- dbgraph/persistent/models.py +222 -0
- dbgraph/persistent/sql_graph_persistent.py +388 -0
- dbgraph/render/__init__.py +0 -0
- dbgraph/render/graph_renderer.py +11 -0
- dbgraph/render/markdown_renderer.py +111 -0
- dbgraph/render/text_renderer.py +11 -0
- dbgraph/search/__init__.py +0 -0
- dbgraph/search/bm25_search_engine.py +33 -0
- dbgraph/search/search_engine.py +15 -0
- dbgraph/utils/__init__.py +0 -0
- dbgraph/utils/singleton.py +37 -0
- dbgraph-0.1.0.dist-info/METADATA +165 -0
- dbgraph-0.1.0.dist-info/RECORD +42 -0
- dbgraph-0.1.0.dist-info/WHEEL +4 -0
dbgraph/entity/aspect.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Aspect:
|
|
7
|
+
"""Represent a set of properties that are related to an aspect
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
name: The name of this aspect
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SemanticAspect(Aspect):
|
|
18
|
+
"""Represent properties related to semantic meaning of the asset
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
description: The natural language description for this asset
|
|
22
|
+
keywords: list of relevant keywords
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
description: str
|
|
26
|
+
keywords: list[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class RTableSchemaAspect(Aspect):
|
|
31
|
+
"""Group of properties relevant to relational databases' table schema
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
pks: list of column names belong to the primary key
|
|
35
|
+
indices: a dictionary which map the table's indices with the columns belong to corresponding indices
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
pks: list[str]
|
|
39
|
+
indices: dict[str, list[str]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class RTableStatisticsAspect(Aspect):
|
|
44
|
+
"""Group of properties relevant to relational databases' table statistics
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
num_columns: number of columns
|
|
48
|
+
num_rows: number of rows
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
num_columns: int
|
|
52
|
+
num_rows: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class RColumnSchemaAspect(Aspect):
|
|
57
|
+
"""Group of properties relevant to RDB's column schema
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
dtype: data type
|
|
61
|
+
is_pk: whether or not this column belongs to primary key
|
|
62
|
+
is_nullable: whether or not this column is nullable
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
dtype: str
|
|
66
|
+
is_pk: bool
|
|
67
|
+
is_nullable: bool
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class RNumericalStatistics:
|
|
72
|
+
"""Statistics for numerical columns
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
mean: Mean value
|
|
76
|
+
std: Standard deviation
|
|
77
|
+
min: Minimum value
|
|
78
|
+
q2: 2nd quantile
|
|
79
|
+
median: Median value
|
|
80
|
+
q3: 3rd quantile
|
|
81
|
+
max: Maximum value
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
mean: float | None = None
|
|
86
|
+
std: float | None = None
|
|
87
|
+
min: float | None = None
|
|
88
|
+
q2: float | None = None
|
|
89
|
+
median: float | None = None
|
|
90
|
+
q3: float | None = None
|
|
91
|
+
max: float | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class RCategoricalStatistics:
|
|
96
|
+
"""Statistics for categorical columns
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
value_counts: top 10 most frequent values in a column and
|
|
100
|
+
its corresponding frequency
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
value_counts: dict[str, int]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class RColumnStatisticsAspect(Aspect):
|
|
108
|
+
"""Group of properties relevant to RDB's column statistics
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
non_null_count: Number of non null records
|
|
112
|
+
null_count: Number of null records
|
|
113
|
+
numerical_stats: numerical statistics of this column if its data type is numeric
|
|
114
|
+
categorical_stats: categorical statistics of this column if its data type is string-like
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
non_null_count: int
|
|
118
|
+
null_count: int
|
|
119
|
+
numerical_stats: RNumericalStatistics | None = None
|
|
120
|
+
categorical_stats: RCategoricalStatistics | None = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class FKBehavior(StrEnum):
|
|
124
|
+
"""Defines the behavior of a foreign key when its referenced row changes.
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
CASCADE: Automatically propagates the change to referencing rows.
|
|
128
|
+
SET_NULL: Sets the foreign key value to ``NULL``.
|
|
129
|
+
RESTRICT: Prevents the operation if referencing rows exist.
|
|
130
|
+
NO_ACTION: Does not perform any action on referencing rows.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
CASCADE = "CASCADE"
|
|
134
|
+
SET_NULL = "SET NULL"
|
|
135
|
+
RESTRICT = "RESTRICT"
|
|
136
|
+
NO_ACTION = "NO ACTION"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class RForeignKeyAspect(Aspect):
|
|
141
|
+
"""Group of properties relevant to foreign keys
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
from_column: the source column
|
|
145
|
+
to_column: referred column
|
|
146
|
+
on_delete: foreign key behavior on delete event
|
|
147
|
+
on_update: foreign key behavior on update event
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
from_column: str
|
|
151
|
+
to_column: str
|
|
152
|
+
on_delete: FKBehavior | str
|
|
153
|
+
on_update: FKBehavior | str
|
dbgraph/entity/asset.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from dbgraph.entity.aspect import (
|
|
4
|
+
Aspect,
|
|
5
|
+
)
|
|
6
|
+
from dbgraph.entity.asset_type import AssetType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Asset:
|
|
11
|
+
"""
|
|
12
|
+
Represent a data asset, such as a table in SQL databases or a document in MongoDB.
|
|
13
|
+
|
|
14
|
+
It acts as a vertex in the graph.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
asset_id: ID of this asset
|
|
18
|
+
name: original name of this asset
|
|
19
|
+
type: type of asset
|
|
20
|
+
aspects: properties aspects belong to this asset
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
asset_id: str
|
|
24
|
+
name: str
|
|
25
|
+
type: AssetType
|
|
26
|
+
aspects: dict[str, Aspect] = field(default_factory=dict)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import rustworkx as rx
|
|
7
|
+
|
|
8
|
+
from dbgraph.entity.asset import Asset
|
|
9
|
+
from dbgraph.entity.link import Link
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DatabaseGraph:
|
|
14
|
+
"""Represents a database as a graph of assets and relationships.
|
|
15
|
+
|
|
16
|
+
A `DatabaseGraph` models the structure of a database using
|
|
17
|
+
assets as nodes and links as relationships between those assets.
|
|
18
|
+
It provides methods for creating, querying, and traversing the
|
|
19
|
+
database structure in a graph-oriented way.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
assets: The collection of `Asset` objects in the graph.
|
|
23
|
+
links: The collection of `Link` objects representing
|
|
24
|
+
relationships between assets.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
assets: list[Asset]
|
|
28
|
+
links: list[Link]
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
self._asset_id_to_node_idx = {a.asset_id: i for i, a in enumerate(self.assets)}
|
|
32
|
+
self._nodes_data = {i: asset for i, asset in enumerate(self.assets)}
|
|
33
|
+
self._edges_data = {i: link for i, link in enumerate(self.links)}
|
|
34
|
+
links_tuples = [
|
|
35
|
+
(
|
|
36
|
+
self._asset_id_to_node_idx[link.source_id],
|
|
37
|
+
self._asset_id_to_node_idx[link.destination_id],
|
|
38
|
+
link,
|
|
39
|
+
)
|
|
40
|
+
for link in self.links
|
|
41
|
+
]
|
|
42
|
+
self._graph = rx.PyGraph()
|
|
43
|
+
self._graph.add_nodes_from(self.assets)
|
|
44
|
+
self._graph.add_edges_from(links_tuples)
|
|
45
|
+
|
|
46
|
+
def _node_idx(self, asset_id: str) -> int:
|
|
47
|
+
try:
|
|
48
|
+
return self._asset_id_to_node_idx[asset_id]
|
|
49
|
+
except KeyError:
|
|
50
|
+
raise KeyError(f"Asset with id={asset_id} not found!")
|
|
51
|
+
|
|
52
|
+
def _neighbors(self, asset_id: str) -> tuple[set[int], list[Link]]:
|
|
53
|
+
"""Return list of asset's indices in the graph and those links"""
|
|
54
|
+
node_idx = self._node_idx(asset_id)
|
|
55
|
+
|
|
56
|
+
# Find links
|
|
57
|
+
links: dict[int, Link] = self._graph.adj(node_idx)
|
|
58
|
+
|
|
59
|
+
# Find assets indices
|
|
60
|
+
asset_indices = {self._node_idx(link.destination_id) for link in links.values()}
|
|
61
|
+
asset_indices = asset_indices.union(
|
|
62
|
+
{self._node_idx(link.source_id) for link in links.values()}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return asset_indices, list(links.values())
|
|
66
|
+
|
|
67
|
+
def get_asset(self, asset_id: str) -> Asset:
|
|
68
|
+
"""Return the asset using asset_id"""
|
|
69
|
+
return self._graph.get_node_data(self._node_idx(asset_id))
|
|
70
|
+
|
|
71
|
+
def get_link(self, src_id: str, dst_id: str) -> Link:
|
|
72
|
+
"""Return the link connect these two Assets"""
|
|
73
|
+
return self._graph.get_edge_data(self._node_idx(src_id), self._node_idx(dst_id))
|
|
74
|
+
|
|
75
|
+
def select_neighbors(self, asset_id: str) -> DatabaseGraph:
|
|
76
|
+
"""Get this node and its neighbors.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
asset_id: ID of the asset to be queried
|
|
80
|
+
|
|
81
|
+
Return:
|
|
82
|
+
subgraph: an instance of `DatabaseGraph` as subgraph
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
asset_indices, links = self._neighbors(asset_id)
|
|
86
|
+
assets = [self._graph.get_node_data(i) for i in asset_indices]
|
|
87
|
+
return DatabaseGraph(assets, links)
|
|
88
|
+
|
|
89
|
+
def select_shortest_paths(self, src_id: str, dst_id: str) -> list[DatabaseGraph]:
|
|
90
|
+
"""
|
|
91
|
+
Find shortest paths between two Assets.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
src_id: ID of the source asset
|
|
95
|
+
dst_id: ID of the destination asset
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
subgraphs: list of `DatabaseGraph` which are the shortest paths
|
|
99
|
+
between source and destination Assets
|
|
100
|
+
"""
|
|
101
|
+
src_idx = self._node_idx(src_id)
|
|
102
|
+
dst_idx = self._node_idx(dst_id)
|
|
103
|
+
|
|
104
|
+
paths = rx.all_shortest_paths(self._graph, src_idx, dst_idx)
|
|
105
|
+
|
|
106
|
+
# Get a list of lists of assets
|
|
107
|
+
assets_lists = [[self._graph.get_node_data(i) for i in path] for path in paths]
|
|
108
|
+
|
|
109
|
+
# Get a list of lists of links
|
|
110
|
+
pairs_lists = [itertools.pairwise(path) for path in paths]
|
|
111
|
+
links_lists = [
|
|
112
|
+
[self._graph.get_edge_data(pair[0], pair[1]) for pair in pairs]
|
|
113
|
+
for pairs in pairs_lists
|
|
114
|
+
]
|
|
115
|
+
return [
|
|
116
|
+
DatabaseGraph(assets, links)
|
|
117
|
+
for assets, links in zip(assets_lists, links_lists)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
def get_neighbors(self, asset_id: str) -> list[Asset]:
|
|
121
|
+
"""Get neighbors assets of an asset
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
asset_id: target asset
|
|
125
|
+
|
|
126
|
+
Return:
|
|
127
|
+
list of neighbor `Asset`s
|
|
128
|
+
"""
|
|
129
|
+
asset_indices, _ = self._neighbors(asset_id)
|
|
130
|
+
# Remove itself
|
|
131
|
+
try:
|
|
132
|
+
asset_indices.remove(self._node_idx(asset_id))
|
|
133
|
+
except KeyError:
|
|
134
|
+
pass
|
|
135
|
+
return [self._graph.get_node_data(i) for i in asset_indices]
|
|
136
|
+
|
|
137
|
+
def find_shortest_paths(self, src_id: str, dst_id: str) -> list[list[Asset]]:
|
|
138
|
+
"""Find shortest paths between two Assets
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
src_id: ID of the source asset
|
|
142
|
+
dst_id: ID of the destination asset
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
assets: list of lists of assets. Each list is a shortest path
|
|
146
|
+
from source Asset to destination Asset
|
|
147
|
+
"""
|
|
148
|
+
src_idx = self._node_idx(src_id)
|
|
149
|
+
dst_idx = self._node_idx(dst_id)
|
|
150
|
+
paths = rx.all_shortest_paths(self._graph, src_idx, dst_idx)
|
|
151
|
+
|
|
152
|
+
return [[self._graph.get_node_data(i) for i in path] for path in paths]
|
dbgraph/entity/link.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from dbgraph.entity.aspect import Aspect, RForeignKeyAspect
|
|
5
|
+
from dbgraph.entity.link_type import LinkType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Link:
|
|
10
|
+
"""
|
|
11
|
+
Represent relationship between two assets.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
link_id: ID of this link
|
|
15
|
+
name: name
|
|
16
|
+
type: type of link
|
|
17
|
+
source_id: ID of the source `Asset`
|
|
18
|
+
destination_id: ID of the destination `Asset`
|
|
19
|
+
aspects: properties aspects belong to this link
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
link_id: str
|
|
23
|
+
name: str
|
|
24
|
+
type: LinkType
|
|
25
|
+
source_id: str
|
|
26
|
+
destination_id: str
|
|
27
|
+
aspects: dict[str, Aspect] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def to_markdown(self, src_name: str, dst_name: str) -> str:
|
|
30
|
+
match self.type:
|
|
31
|
+
case LinkType.CONTAIN:
|
|
32
|
+
return f"- Column `{dst_name}` belongs to table `{src_name}`\n"
|
|
33
|
+
case LinkType.FOREIGN_KEY:
|
|
34
|
+
fk_aspect = cast(
|
|
35
|
+
RForeignKeyAspect, self.aspects["foreign_key_properties"]
|
|
36
|
+
)
|
|
37
|
+
return f"- `{src_name}`.`{fk_aspect.from_column}` -> `{dst_name}`.`{fk_aspect.to_column}`\n"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dbgraph import LinkType
|
|
4
|
+
from dbgraph.entity.asset import Asset
|
|
5
|
+
from dbgraph.entity.asset_type import AssetType
|
|
6
|
+
from dbgraph.entity.dbgraph import DatabaseGraph
|
|
7
|
+
from dbgraph.entity.link import Link
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RDatabaseGraph(DatabaseGraph):
|
|
11
|
+
"""Graph for Relational Database"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def from_graph(graph: DatabaseGraph) -> RDatabaseGraph:
|
|
15
|
+
return RDatabaseGraph(graph.assets, graph.links)
|
|
16
|
+
|
|
17
|
+
def get_columns(self, table_id: str) -> list[Asset]:
|
|
18
|
+
"""Get list of assets which are columns belong to this table
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
table_id: ID of the target table
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
assets: columns belong to this table
|
|
25
|
+
"""
|
|
26
|
+
neighbors = self.get_neighbors(table_id)
|
|
27
|
+
return [n for n in neighbors if n.type == AssetType.RCOLUMN]
|
|
28
|
+
|
|
29
|
+
def get_tables(self) -> list[Asset]:
|
|
30
|
+
"""Get all the tables in the graph"""
|
|
31
|
+
return [a for a in self.assets if a.type == AssetType.RTABLE]
|
|
32
|
+
|
|
33
|
+
def get_foreign_keys(self) -> list[Link]:
|
|
34
|
+
"""Get all foreign keys in the graph"""
|
|
35
|
+
return [link for link in self.links if link.type == LinkType.FOREIGN_KEY]
|
|
36
|
+
|
|
37
|
+
def get_connected_tables(self, table_id: str) -> list[Asset]:
|
|
38
|
+
"""Get all connected table
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
table_id: target table
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
assets: list of references table
|
|
45
|
+
"""
|
|
46
|
+
neighbors = self.get_neighbors(table_id)
|
|
47
|
+
return [n for n in neighbors if n.type == AssetType.RTABLE]
|
|
48
|
+
|
|
49
|
+
def select_connected_tables(self, table_id: str) -> RDatabaseGraph:
|
|
50
|
+
"""Get references tables with their columns
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
table_id: target table
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
subgraph: the subgraph of which include connected tables and their columns
|
|
57
|
+
"""
|
|
58
|
+
refs_tables = self.get_connected_tables(table_id)
|
|
59
|
+
assets: list[Asset] = [table for table in refs_tables]
|
|
60
|
+
target_table: Asset = self.get_asset(table_id)
|
|
61
|
+
target_table_columns = self.get_columns(table_id)
|
|
62
|
+
assets.append(target_table)
|
|
63
|
+
assets.extend(target_table_columns)
|
|
64
|
+
links: list[Link] = [
|
|
65
|
+
self.get_link(target_table.asset_id, column.asset_id)
|
|
66
|
+
for column in target_table_columns
|
|
67
|
+
]
|
|
68
|
+
for table in refs_tables:
|
|
69
|
+
fk_link = self.get_link(table_id, table.asset_id)
|
|
70
|
+
links.append(fk_link)
|
|
71
|
+
columns = self.get_columns(table.asset_id)
|
|
72
|
+
for column in columns:
|
|
73
|
+
assets.append(column)
|
|
74
|
+
link = self.get_link(table.asset_id, column.asset_id)
|
|
75
|
+
links.append(link)
|
|
76
|
+
return RDatabaseGraph(assets, links)
|
|
File without changes
|
dbgraph/event/event.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Event:
|
|
6
|
+
"""Base event used in DBGraph"""
|
|
7
|
+
|
|
8
|
+
message: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ProgressEvent(Event):
|
|
13
|
+
"""Event with progress"""
|
|
14
|
+
|
|
15
|
+
target: float
|
|
16
|
+
current: float
|
|
17
|
+
|
|
18
|
+
def get_percentage(self) -> float:
|
|
19
|
+
"""Return the progress of the task in percentage"""
|
|
20
|
+
return self.current / self.target
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from dbgraph.event.event import Event
|
|
5
|
+
from dbgraph.event.event_handler import EventHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventBus:
|
|
9
|
+
"""Store events in a queue and manage a background thread to distribute events"""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.queue = queue.Queue()
|
|
13
|
+
self.handlers: dict[type[Event], set[EventHandler]] = {}
|
|
14
|
+
self._thread: threading.Thread | None = None
|
|
15
|
+
self._stop_event = threading.Event()
|
|
16
|
+
|
|
17
|
+
def subscribe(self, event_type: type[Event], handler: EventHandler):
|
|
18
|
+
"""Subscribe a handler to specific type of event"""
|
|
19
|
+
if event_type in self.handlers:
|
|
20
|
+
self.handlers[event_type].add(handler)
|
|
21
|
+
else:
|
|
22
|
+
self.handlers[event_type] = {handler}
|
|
23
|
+
|
|
24
|
+
def unsubscribe(self, event_type: type[Event], handler: EventHandler):
|
|
25
|
+
"""Unsubscribe a handler from listening to specific event type"""
|
|
26
|
+
self.handlers[event_type].remove(handler)
|
|
27
|
+
|
|
28
|
+
def publish(self, event: Event):
|
|
29
|
+
"""Push an event into the event queue"""
|
|
30
|
+
self.queue.put(event)
|
|
31
|
+
|
|
32
|
+
def _dispatch(self):
|
|
33
|
+
"""Get the latest event and let the appropriate handler process it"""
|
|
34
|
+
event = self.queue.get()
|
|
35
|
+
for handler in self.handlers[type(event)]:
|
|
36
|
+
handler.handle(event)
|
|
37
|
+
|
|
38
|
+
def _run(self):
|
|
39
|
+
"""Run the event loop"""
|
|
40
|
+
while not self._stop_event.is_set():
|
|
41
|
+
try:
|
|
42
|
+
self._dispatch()
|
|
43
|
+
except queue.Empty:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
def start(self):
|
|
47
|
+
"""Start the event background thread"""
|
|
48
|
+
if self._thread is not None and self._thread.is_alive():
|
|
49
|
+
return
|
|
50
|
+
self._stop_event.clear()
|
|
51
|
+
self._thread = threading.Thread(
|
|
52
|
+
target=self._run, name="dbgraph-event-bus", daemon=True
|
|
53
|
+
)
|
|
54
|
+
self._thread.start()
|
|
55
|
+
|
|
56
|
+
def stop(self):
|
|
57
|
+
"""Stop the event background thread"""
|
|
58
|
+
self._stop_event.set()
|
|
59
|
+
if self._thread is not None:
|
|
60
|
+
self._thread.join()
|
|
61
|
+
self._thread = None
|
dbgraph/io/__init__.py
ADDED
|
File without changes
|