diff --git a/.gitignore b/.gitignore index b6e4761..b97131e 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dmypy.json # Pyre type checker .pyre/ + +.idea/ diff --git a/README.md b/README.md index d717ad7..f94cf41 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# dag-dependency-resolver \ No newline at end of file +# dag-dependency-resolver + +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) +[![Static typing: mypy](https://img.shields.io/badge/%20static-mypy-%3167400?style=flat&labelColor=A09711)](http://mypy-lang.org/) diff --git a/extras/__init__.py b/extras/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extras/yaml/__init__.py b/extras/yaml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extras/yaml/argo_templates/__init__.py b/extras/yaml/argo_templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4e42573 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 120 +target-version = ['py38'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..3789e12 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,6 @@ +pytest==7.1.2 +unittest +black==22.3.0 +isort==5.10.1 +flake8==5.0.4 +mypy==0.990 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..968baba --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +[tool:isort] +line_length=120 +multi_line_output=3 +force_grid_wrap=True +indent=4 +use_parentheses=True +include_trailing_comma=True +lines_after_imports=2 +combine_as_imports=True +skip=venv + +[flake8] +ignore = C901, W503 +exclude = + .git, + venv, + .venv, + ./venv, + __pycache__ + +max-line-length = 120 +max-complexity = 10 +show-source = True +statistics = True + + +[mypy] +python_version = 3.8 +show_error_codes = True +show_error_context = True +pretty = True +warn_unused_configs = True +warn_unused_ignores = True +ignore_missing_imports = True +warn_unreachable = True +warn_return_any = True +warn_redundant_casts = True +no_implicit_optional = True +disallow_untyped_decorators = True +check_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_defs = True +disallow_untyped_calls = True +disallow_subclassing_any = False +disallow_any_generics = True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/enums/__init__.py b/src/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/enums/node_status.py b/src/enums/node_status.py new file mode 100644 index 0000000..16dd102 --- /dev/null +++ b/src/enums/node_status.py @@ -0,0 +1,9 @@ +from enum import ( + Enum, + auto, +) + + +class NodeStatus(Enum): + VISITED = auto() + UNVISITED = auto() diff --git a/src/graph.py b/src/graph.py new file mode 100644 index 0000000..7542358 --- /dev/null +++ b/src/graph.py @@ -0,0 +1,42 @@ +from dataclasses import ( + dataclass, + field, +) +from typing import ( + Dict, + List, + Text, +) + +from src.enums.node_status import NodeStatus +from src.node import Node + + +@dataclass +class DependencyGraph: + nodes: Dict[Text, Node] = field(default_factory=dict) + + def add_node(self, name: str, parents: List[Text]) -> None: + self.nodes[name] = Node(name=name, parents=parents) + + def get_dependencies_order(self) -> List[str]: + dependencies_order: List[str] = [] + + for node in self.nodes.values(): + self._solve_deps(node, dependencies_order) + + self._reset_nodes_status() + return dependencies_order + + def _solve_deps(self, node: Node, dependencies_order: List[Text]) -> None: + if node.status != NodeStatus.VISITED: + for parent in node.parents: + self._solve_deps(self.nodes[parent], dependencies_order) + + node.status = NodeStatus.VISITED + + dependencies_order.append(node.name) + + def _reset_nodes_status(self) -> None: + for node in self.nodes.values(): + node.status = NodeStatus.UNVISITED diff --git a/src/node.py b/src/node.py new file mode 100644 index 0000000..8935de1 --- /dev/null +++ b/src/node.py @@ -0,0 +1,17 @@ +from dataclasses import ( + dataclass, + field, +) +from typing import ( + List, + Text, +) + +from src.enums.node_status import NodeStatus + + +@dataclass +class Node: + name: Text + parents: List[Text] = field(default_factory=list) + status: NodeStatus = field(default=NodeStatus.UNVISITED) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_graph.py b/tests/test_graph.py new file mode 100644 index 0000000..eac05ed --- /dev/null +++ b/tests/test_graph.py @@ -0,0 +1,32 @@ +import unittest + +from src.graph import DependencyGraph +from src.node import Node + + +class TestStringMethods(unittest.TestCase): + def setUp(self) -> None: + self.dependency_graph = DependencyGraph() + + def test_add_node(self) -> None: + self.dependency_graph.add_node(name="c", parents=["a", "b"]) + + self.assertDictEqual(self.dependency_graph.nodes, {"c": Node(name="c", parents=["a", "b"])}) + + def test_get_dependencies_order(self) -> None: + self.dependency_graph.add_node(name="a", parents=["b", "c"]) + self.dependency_graph.add_node(name="b", parents=[]) + self.dependency_graph.add_node(name="c", parents=[]) + + dependencies_order = self.dependency_graph.get_dependencies_order() + + self.assertListEqual(dependencies_order, ["b", "c", "a"]) + + def test_get_dependencies_order_disconnected_graph(self) -> None: + self.dependency_graph.add_node(name="a", parents=[]) + self.dependency_graph.add_node(name="b", parents=[]) + self.dependency_graph.add_node(name="c", parents=[]) + + dependencies_order = self.dependency_graph.get_dependencies_order() + + self.assertListEqual(dependencies_order, ["a", "b", "c"])