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.
@@ -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
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class GraphNodeInterface(ABC):
7
+ @abstractmethod
8
+ def get_graph_id(self) -> str | int:
9
+ 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.4.7)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+
3
+ [gui_scripts]
4
+