syrtis-python-graph 0.0.3__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.
@@ -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,85 @@
1
+ # syrtis-python-graph
2
+
3
+ Version: 0.0.3
4
+
5
+ ## Table of Contents
6
+
7
+ - [Status Compatibility](#status-compatibility)
8
+ - [Tests](#tests)
9
+ - [Roadmap](#roadmap)
10
+ - [Useful Links](#useful-links)
11
+
12
+
13
+ ## Status & Compatibility
14
+
15
+ **Maturity**: Production-ready
16
+
17
+ **Python Support**: >=3.10
18
+
19
+ **OS Support**: Linux, macOS, Windows
20
+
21
+ **Status**: Actively maintained
22
+
23
+ ## Tests
24
+
25
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
26
+
27
+ ### Installation
28
+
29
+ First, install the required testing dependencies:
30
+ ```bash
31
+ .venv/bin/python -m pip install pytest pytest-cov
32
+ ```
33
+
34
+ ### Basic Usage
35
+
36
+ Run all tests with coverage:
37
+ ```bash
38
+ .venv/bin/python -m pytest --cov --cov-report=html
39
+ ```
40
+
41
+ ### Common Commands
42
+ ```bash
43
+ # Run tests with coverage for a specific module
44
+ .venv/bin/python -m pytest --cov=your_module
45
+
46
+ # Show which lines are not covered
47
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
48
+
49
+ # Generate an HTML coverage report
50
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
51
+
52
+ # Combine terminal and HTML reports
53
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
54
+
55
+ # Run specific test file with coverage
56
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
57
+ ```
58
+
59
+ ### Viewing HTML Reports
60
+
61
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
62
+
63
+ ### Coverage Threshold
64
+
65
+ To enforce a minimum coverage percentage:
66
+ ```bash
67
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
68
+ ```
69
+
70
+ This will cause the test suite to fail if coverage drops below 80%.
71
+
72
+ ## Known Limitations & Roadmap
73
+
74
+ Current limitations and planned features are tracked in the GitHub issues.
75
+
76
+ See the [project roadmap](https://github.com/wexample/python-python_graph/issues) for upcoming features and improvements.
77
+
78
+ ## Useful Links
79
+
80
+ - **Homepage**: https://github.com/wexample/python-python-graph
81
+ - **Documentation**: [docs.wexample.com](https://docs.wexample.com)
82
+ - **Issue Tracker**: https://github.com/wexample/python-python-graph/issues
83
+ - **Discussions**: https://github.com/wexample/python-python-graph/discussions
84
+ - **PyPI**: [pypi.org/project/syrtis-python-graph](https://pypi.org/project/syrtis-python-graph/)
85
+
@@ -0,0 +1,78 @@
1
+ [build-system]
2
+ requires = [
3
+ "pdm-backend",
4
+ ]
5
+ build-backend = "pdm.backend"
6
+
7
+ [project]
8
+ name = "syrtis-python-graph"
9
+ version = "0.0.3"
10
+ authors = [
11
+ { name = "weeger", email = "contact@syrtis.ai" },
12
+ ]
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = []
20
+
21
+ [project.readme]
22
+ file = "README.md"
23
+ content-type = "text/markdown"
24
+
25
+ [project.license]
26
+ text = "MIT"
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest",
31
+ "pytest-cov",
32
+ ]
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = [
36
+ "*",
37
+ ]
38
+ exclude = [
39
+ "syrtis_python_graph.testing*",
40
+ ]
41
+
42
+ [tool.pdm]
43
+ distribution = true
44
+
45
+ [tool.pdm.build]
46
+ package-dir = "src"
47
+ packages = [
48
+ { include = "syrtis_python_graph", from = "src" },
49
+ ]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = [
53
+ "tests",
54
+ ]
55
+ pythonpath = [
56
+ "src",
57
+ ]
58
+
59
+ [tool.coverage.run]
60
+ source = [
61
+ "syrtis_python_graph",
62
+ ]
63
+ omit = [
64
+ "*/tests/*",
65
+ "*/.venv/*",
66
+ "*/venv/*",
67
+ ]
68
+
69
+ [tool.coverage.report]
70
+ exclude_lines = [
71
+ "pragma: no cover",
72
+ "def __repr__",
73
+ "raise AssertionError",
74
+ "raise NotImplementedError",
75
+ "if __name__ == .__main__.:",
76
+ "if TYPE_CHECKING:",
77
+ "@abstractmethod",
78
+ ]
@@ -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
+ ]
@@ -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
@@ -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
@@ -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))
@@ -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, [])
@@ -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
+ ]