syrtis-python-graph 0.0.3__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.
- syrtis_python_graph/__init__.py +19 -0
- syrtis_python_graph/checker/__init__.py +0 -0
- syrtis_python_graph/checker/graph_readiness_checker.py +23 -0
- syrtis_python_graph/contract/__init__.py +0 -0
- syrtis_python_graph/contract/graph_edge_interface.py +25 -0
- syrtis_python_graph/contract/graph_node_interface.py +9 -0
- syrtis_python_graph/detector/__init__.py +0 -0
- syrtis_python_graph/detector/graph_cycle_detector.py +73 -0
- syrtis_python_graph/model/__init__.py +0 -0
- syrtis_python_graph/model/cycle_detection_result.py +15 -0
- syrtis_python_graph/model/graph_definition.py +42 -0
- syrtis_python_graph/py.typed +0 -0
- syrtis_python_graph/resolver/__init__.py +0 -0
- syrtis_python_graph/resolver/graph_resolver.py +51 -0
- syrtis_python_graph-0.0.3.dist-info/METADATA +99 -0
- syrtis_python_graph-0.0.3.dist-info/RECORD +18 -0
- syrtis_python_graph-0.0.3.dist-info/WHEEL +4 -0
- syrtis_python_graph-0.0.3.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from syrtis_python_graph.checker.graph_readiness_checker import GraphReadinessChecker
|
|
4
|
+
from syrtis_python_graph.contract.graph_edge_interface import GraphEdgeInterface
|
|
5
|
+
from syrtis_python_graph.contract.graph_node_interface import GraphNodeInterface
|
|
6
|
+
from syrtis_python_graph.detector.graph_cycle_detector import GraphCycleDetector
|
|
7
|
+
from syrtis_python_graph.model.cycle_detection_result import CycleDetectionResult
|
|
8
|
+
from syrtis_python_graph.model.graph_definition import GraphDefinition
|
|
9
|
+
from syrtis_python_graph.resolver.graph_resolver import GraphResolver
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"GraphNodeInterface",
|
|
13
|
+
"GraphEdgeInterface",
|
|
14
|
+
"GraphDefinition",
|
|
15
|
+
"CycleDetectionResult",
|
|
16
|
+
"GraphCycleDetector",
|
|
17
|
+
"GraphReadinessChecker",
|
|
18
|
+
"GraphResolver",
|
|
19
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from syrtis_python_graph.model.graph_definition import GraphDefinition
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GraphReadinessChecker:
|
|
7
|
+
def is_ready(
|
|
8
|
+
self,
|
|
9
|
+
node_id: str | int,
|
|
10
|
+
completed_ids: list[str | int],
|
|
11
|
+
definition: GraphDefinition,
|
|
12
|
+
back_edges: list[tuple[str | int, str | int]] | None = None,
|
|
13
|
+
) -> bool:
|
|
14
|
+
completed_set = {str(i) for i in completed_ids}
|
|
15
|
+
back_edge_set = {(src, tgt) for src, tgt in (back_edges or [])}
|
|
16
|
+
|
|
17
|
+
for edge in definition.get_incoming_edges(node_id):
|
|
18
|
+
if (edge.get_source_node_id(), edge.get_target_node_id()) in back_edge_set:
|
|
19
|
+
continue
|
|
20
|
+
if str(edge.get_source_node_id()) not in completed_set:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GraphEdgeInterface(ABC):
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def get_graph_id(self) -> str | int:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def get_source_node_id(self) -> str | int:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_source_port(self) -> str | None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_target_node_id(self) -> str | int:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get_target_port(self) -> str | None:
|
|
25
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from syrtis_python_graph.model.cycle_detection_result import CycleDetectionResult
|
|
4
|
+
from syrtis_python_graph.model.graph_definition import GraphDefinition
|
|
5
|
+
|
|
6
|
+
_WHITE = 0
|
|
7
|
+
_GRAY = 1
|
|
8
|
+
_BLACK = 2
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GraphCycleDetector:
|
|
12
|
+
def detect(self, definition: GraphDefinition) -> CycleDetectionResult:
|
|
13
|
+
color: dict[str | int, int] = {}
|
|
14
|
+
parent: dict[str | int, str | int | None] = {}
|
|
15
|
+
back_edges: list[tuple[str | int, str | int]] = []
|
|
16
|
+
cycles: list[list[str | int]] = []
|
|
17
|
+
|
|
18
|
+
for node_id in definition.get_nodes():
|
|
19
|
+
color[node_id] = _WHITE
|
|
20
|
+
parent[node_id] = None
|
|
21
|
+
|
|
22
|
+
for node_id in definition.get_nodes():
|
|
23
|
+
if color[node_id] == _WHITE:
|
|
24
|
+
self._dfs(node_id, definition, color, parent, back_edges, cycles)
|
|
25
|
+
|
|
26
|
+
nodes_in_cycles: set[str | int] = {
|
|
27
|
+
node_id for cycle in cycles for node_id in cycle
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return CycleDetectionResult(
|
|
31
|
+
has_cycles=bool(back_edges),
|
|
32
|
+
cycles=cycles,
|
|
33
|
+
back_edges=back_edges,
|
|
34
|
+
nodes_in_cycles=nodes_in_cycles,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _dfs(
|
|
38
|
+
self,
|
|
39
|
+
node_id: str | int,
|
|
40
|
+
definition: GraphDefinition,
|
|
41
|
+
color: dict[str | int, int],
|
|
42
|
+
parent: dict[str | int, str | int | None],
|
|
43
|
+
back_edges: list[tuple[str | int, str | int]],
|
|
44
|
+
cycles: list[list[str | int]],
|
|
45
|
+
) -> None:
|
|
46
|
+
color[node_id] = _GRAY
|
|
47
|
+
|
|
48
|
+
for edge in definition.get_outgoing_edges(node_id):
|
|
49
|
+
target_id = edge.get_target_node_id()
|
|
50
|
+
|
|
51
|
+
if color.get(target_id) == _GRAY:
|
|
52
|
+
back_edges.append((node_id, target_id))
|
|
53
|
+
cycles.append(self._extract_cycle(node_id, target_id, parent))
|
|
54
|
+
elif color.get(target_id, _WHITE) == _WHITE:
|
|
55
|
+
parent[target_id] = node_id
|
|
56
|
+
self._dfs(target_id, definition, color, parent, back_edges, cycles)
|
|
57
|
+
|
|
58
|
+
color[node_id] = _BLACK
|
|
59
|
+
|
|
60
|
+
def _extract_cycle(
|
|
61
|
+
self,
|
|
62
|
+
from_id: str | int,
|
|
63
|
+
to_id: str | int,
|
|
64
|
+
parent: dict[str | int, str | int | None],
|
|
65
|
+
) -> list[str | int]:
|
|
66
|
+
cycle: list[str | int] = [from_id]
|
|
67
|
+
current: str | int = from_id
|
|
68
|
+
|
|
69
|
+
while current != to_id:
|
|
70
|
+
current = parent[current] # type: ignore[assignment]
|
|
71
|
+
cycle.append(current)
|
|
72
|
+
|
|
73
|
+
return list(reversed(cycle))
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class CycleDetectionResult:
|
|
8
|
+
back_edges: list[tuple[str | int, str | int]]
|
|
9
|
+
cycles: list[list[str | int]]
|
|
10
|
+
has_cycles: bool
|
|
11
|
+
|
|
12
|
+
nodes_in_cycles: set[str | int] = field(default_factory=set)
|
|
13
|
+
|
|
14
|
+
def is_back_edge(self, source_id: str | int, target_id: str | int) -> bool:
|
|
15
|
+
return (source_id, target_id) in self.back_edges
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from syrtis_python_graph.contract.graph_edge_interface import GraphEdgeInterface
|
|
4
|
+
from syrtis_python_graph.contract.graph_node_interface import GraphNodeInterface
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GraphDefinition:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self._nodes: dict[str | int, GraphNodeInterface] = {}
|
|
10
|
+
self._edges: dict[str | int, GraphEdgeInterface] = {}
|
|
11
|
+
self._outgoing: dict[str | int, list[GraphEdgeInterface]] = {}
|
|
12
|
+
self._incoming: dict[str | int, list[GraphEdgeInterface]] = {}
|
|
13
|
+
|
|
14
|
+
def add_edge(self, edge: GraphEdgeInterface) -> None:
|
|
15
|
+
edge_id = edge.get_graph_id()
|
|
16
|
+
source_id = edge.get_source_node_id()
|
|
17
|
+
target_id = edge.get_target_node_id()
|
|
18
|
+
|
|
19
|
+
self._edges[edge_id] = edge
|
|
20
|
+
self._outgoing.setdefault(source_id, []).append(edge)
|
|
21
|
+
self._incoming.setdefault(target_id, []).append(edge)
|
|
22
|
+
|
|
23
|
+
def add_node(self, node: GraphNodeInterface) -> None:
|
|
24
|
+
node_id = node.get_graph_id()
|
|
25
|
+
self._nodes[node_id] = node
|
|
26
|
+
self._outgoing.setdefault(node_id, [])
|
|
27
|
+
self._incoming.setdefault(node_id, [])
|
|
28
|
+
|
|
29
|
+
def get_edges(self) -> dict[str | int, GraphEdgeInterface]:
|
|
30
|
+
return self._edges
|
|
31
|
+
|
|
32
|
+
def get_incoming_edges(self, node_id: str | int) -> list[GraphEdgeInterface]:
|
|
33
|
+
return self._incoming.get(node_id, [])
|
|
34
|
+
|
|
35
|
+
def get_node(self, node_id: str | int) -> GraphNodeInterface | None:
|
|
36
|
+
return self._nodes.get(node_id)
|
|
37
|
+
|
|
38
|
+
def get_nodes(self) -> dict[str | int, GraphNodeInterface]:
|
|
39
|
+
return self._nodes
|
|
40
|
+
|
|
41
|
+
def get_outgoing_edges(self, node_id: str | int) -> list[GraphEdgeInterface]:
|
|
42
|
+
return self._outgoing.get(node_id, [])
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from syrtis_python_graph.contract.graph_node_interface import GraphNodeInterface
|
|
4
|
+
from syrtis_python_graph.model.graph_definition import GraphDefinition
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GraphResolver:
|
|
8
|
+
def __init__(self, definition: GraphDefinition) -> None:
|
|
9
|
+
self._definition = definition
|
|
10
|
+
|
|
11
|
+
def get_end_nodes(self) -> list[GraphNodeInterface]:
|
|
12
|
+
return [
|
|
13
|
+
node
|
|
14
|
+
for node in self._definition.get_nodes().values()
|
|
15
|
+
if not self._definition.get_outgoing_edges(node.get_graph_id())
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def get_predecessor_ids(self, node_id: str | int) -> list[str | int]:
|
|
19
|
+
return [
|
|
20
|
+
edge.get_source_node_id()
|
|
21
|
+
for edge in self._definition.get_incoming_edges(node_id)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def get_predecessors(self, node_id: str | int) -> list[GraphNodeInterface]:
|
|
25
|
+
return [
|
|
26
|
+
node
|
|
27
|
+
for edge in self._definition.get_incoming_edges(node_id)
|
|
28
|
+
if (node := self._definition.get_node(edge.get_source_node_id()))
|
|
29
|
+
is not None
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def get_start_nodes(self) -> list[GraphNodeInterface]:
|
|
33
|
+
return [
|
|
34
|
+
node
|
|
35
|
+
for node in self._definition.get_nodes().values()
|
|
36
|
+
if not self._definition.get_incoming_edges(node.get_graph_id())
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def get_successor_ids(self, node_id: str | int) -> list[str | int]:
|
|
40
|
+
return [
|
|
41
|
+
edge.get_target_node_id()
|
|
42
|
+
for edge in self._definition.get_outgoing_edges(node_id)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def get_successors(self, node_id: str | int) -> list[GraphNodeInterface]:
|
|
46
|
+
return [
|
|
47
|
+
node
|
|
48
|
+
for edge in self._definition.get_outgoing_edges(node_id)
|
|
49
|
+
if (node := self._definition.get_node(edge.get_target_node_id()))
|
|
50
|
+
is not None
|
|
51
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: syrtis-python-graph
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Author-Email: weeger <contact@syrtis.ai>
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# syrtis-python-graph
|
|
16
|
+
|
|
17
|
+
Version: 0.0.3
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Status Compatibility](#status-compatibility)
|
|
22
|
+
- [Tests](#tests)
|
|
23
|
+
- [Roadmap](#roadmap)
|
|
24
|
+
- [Useful Links](#useful-links)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Status & Compatibility
|
|
28
|
+
|
|
29
|
+
**Maturity**: Production-ready
|
|
30
|
+
|
|
31
|
+
**Python Support**: >=3.10
|
|
32
|
+
|
|
33
|
+
**OS Support**: Linux, macOS, Windows
|
|
34
|
+
|
|
35
|
+
**Status**: Actively maintained
|
|
36
|
+
|
|
37
|
+
## Tests
|
|
38
|
+
|
|
39
|
+
This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
|
|
40
|
+
|
|
41
|
+
### Installation
|
|
42
|
+
|
|
43
|
+
First, install the required testing dependencies:
|
|
44
|
+
```bash
|
|
45
|
+
.venv/bin/python -m pip install pytest pytest-cov
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Basic Usage
|
|
49
|
+
|
|
50
|
+
Run all tests with coverage:
|
|
51
|
+
```bash
|
|
52
|
+
.venv/bin/python -m pytest --cov --cov-report=html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Common Commands
|
|
56
|
+
```bash
|
|
57
|
+
# Run tests with coverage for a specific module
|
|
58
|
+
.venv/bin/python -m pytest --cov=your_module
|
|
59
|
+
|
|
60
|
+
# Show which lines are not covered
|
|
61
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
|
|
62
|
+
|
|
63
|
+
# Generate an HTML coverage report
|
|
64
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=html
|
|
65
|
+
|
|
66
|
+
# Combine terminal and HTML reports
|
|
67
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
|
|
68
|
+
|
|
69
|
+
# Run specific test file with coverage
|
|
70
|
+
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Viewing HTML Reports
|
|
74
|
+
|
|
75
|
+
After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
|
|
76
|
+
|
|
77
|
+
### Coverage Threshold
|
|
78
|
+
|
|
79
|
+
To enforce a minimum coverage percentage:
|
|
80
|
+
```bash
|
|
81
|
+
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This will cause the test suite to fail if coverage drops below 80%.
|
|
85
|
+
|
|
86
|
+
## Known Limitations & Roadmap
|
|
87
|
+
|
|
88
|
+
Current limitations and planned features are tracked in the GitHub issues.
|
|
89
|
+
|
|
90
|
+
See the [project roadmap](https://github.com/wexample/python-python_graph/issues) for upcoming features and improvements.
|
|
91
|
+
|
|
92
|
+
## Useful Links
|
|
93
|
+
|
|
94
|
+
- **Homepage**: https://github.com/wexample/python-python-graph
|
|
95
|
+
- **Documentation**: [docs.wexample.com](https://docs.wexample.com)
|
|
96
|
+
- **Issue Tracker**: https://github.com/wexample/python-python-graph/issues
|
|
97
|
+
- **Discussions**: https://github.com/wexample/python-python-graph/discussions
|
|
98
|
+
- **PyPI**: [pypi.org/project/syrtis-python-graph](https://pypi.org/project/syrtis-python-graph/)
|
|
99
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
syrtis_python_graph-0.0.3.dist-info/METADATA,sha256=bcmqvycR20pjHI3-zLS9eNUwBzvEy1qxpdlsWczic1I,2695
|
|
2
|
+
syrtis_python_graph-0.0.3.dist-info/WHEEL,sha256=Wb0ASbVj8JvWHpOiIpPi7ucfIgJeCi__PzivviEAQFc,90
|
|
3
|
+
syrtis_python_graph-0.0.3.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
syrtis_python_graph/__init__.py,sha256=XGbAWC4NnfJoovwPc_wulWYYFJTbIaSD8_ru8ds5-mk,782
|
|
5
|
+
syrtis_python_graph/checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
syrtis_python_graph/checker/graph_readiness_checker.py,sha256=NFWxoK5mwpYsGVtKfGl80iQRC_o5di0W9VRdgyx7AOU,779
|
|
7
|
+
syrtis_python_graph/contract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
syrtis_python_graph/contract/graph_edge_interface.py,sha256=3sMu3yM0z2W9x0Y9U0jZM9F-cCDLagQYqmpQS5m6w28,499
|
|
9
|
+
syrtis_python_graph/contract/graph_node_interface.py,sha256=GZPfOX47N0UZN3vrHEHIV3LSvcsYH8DhTikVrj-oS4g,179
|
|
10
|
+
syrtis_python_graph/detector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
syrtis_python_graph/detector/graph_cycle_detector.py,sha256=xG0DTNnNlWRNSWgMyrvSosIOtudUmur-1aGiqzs9Zr4,2379
|
|
12
|
+
syrtis_python_graph/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
syrtis_python_graph/model/cycle_detection_result.py,sha256=JgFyJWSSG0GEsBCPN9x37ge-1xU_fp4ij_e_Ys-32pc,440
|
|
14
|
+
syrtis_python_graph/model/graph_definition.py,sha256=IiHCNQpVGUsh03oruSL8kQU7afbvrzgBk1I-vZ3A5PY,1656
|
|
15
|
+
syrtis_python_graph/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
syrtis_python_graph/resolver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
syrtis_python_graph/resolver/graph_resolver.py,sha256=VcOTiqCsPeeCPZpbnz7yF2vpoQKM4k6J-5B--1AzQUM,1798
|
|
18
|
+
syrtis_python_graph-0.0.3.dist-info/RECORD,,
|