diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..293f488 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bc7282 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +.static_storage/ +.media/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +sample/ +.vscode/ +.pytest_cache/ diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 0000000..e69de29 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..918079e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[WIP] Tool for writing modular shell scripts diff --git a/msh/__init__.py b/msh/__init__.py new file mode 100644 index 0000000..a8fc4f9 --- /dev/null +++ b/msh/__init__.py @@ -0,0 +1,3 @@ +__version__ = '0.0.3' + +__description__ = 'Tool for writing modular shell scripts' diff --git a/msh/cli.py b/msh/cli.py new file mode 100644 index 0000000..e1b01e3 --- /dev/null +++ b/msh/cli.py @@ -0,0 +1,29 @@ +import sys +from argparse import ArgumentParser +from pathlib import Path + +from . import __description__ as desc + + +def entrypoint(): + parser = ArgumentParser( + prog='msh', + description=desc, + ) + + parser.add_argument( + '--entry', + required=True, + type=Path, + ) + + parser.add_argument( + '--output', + type=Path, + default='build.sh' + ) + + options = parser.parse_args(sys.argv[1:]) + + if not options.entry.exists(): + sys.stdout.write('Please provide existing path\n') diff --git a/msh/graph.py b/msh/graph.py new file mode 100644 index 0000000..8c2b3f3 --- /dev/null +++ b/msh/graph.py @@ -0,0 +1,28 @@ +from .node import Node + + +class Graph: + def __init__(self): + self._graph = {} + + def add_node(self, key, params=None): + if not isinstance(key, str): + raise TypeError('key should a str') + + if key in self._graph: + return self._graph[key] + + self._graph[key] = Node(key, params) + return self._graph[key] + + def get_node(self, key): + return self._graph.get(key) + + def add_edge(self, start, end): + self.add_node(start).add_edge(self.add_node(end)) + + def get_connections(self, key): + if key not in self._graph: + return [] + + return self._graph[key].get_connections() diff --git a/msh/node.py b/msh/node.py new file mode 100644 index 0000000..d5fd56d --- /dev/null +++ b/msh/node.py @@ -0,0 +1,22 @@ +class Node: + def __init__(self, key, params=None): + if not isinstance(key, str): + raise TypeError('key should be str') + + self.key = key + if params is None: + params = {} + self.params = params + self.edges = [] + + @property + def id(self): + return self.key + + def add_edge(self, node): + assert isinstance(node, Node) + if node not in self.edges: + self.edges.append(node) + + def get_connections(self): + return self.edges diff --git a/msh/resolve.py b/msh/resolve.py new file mode 100644 index 0000000..47a654f --- /dev/null +++ b/msh/resolve.py @@ -0,0 +1,24 @@ +from .node import Node + + +def resolve(node): + if not isinstance(node, Node): + raise ValueError('value should be instance of Node') + + unresolved, resolved = [], [] + + def traverse(node, resolved, unresolved): + unresolved.append(node) + connections = node.get_connections() + for edge in connections: + if edge not in resolved: + if edge in unresolved: + return + traverse(edge, resolved, unresolved) + + resolved.append(node) + unresolved.remove(node) + + traverse(node, resolved, unresolved) + + return resolved diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3c1235c --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +import io +import os +import re +import sys + +from setuptools import setup + + +def get_variable(var_name): + regex = "__{var_name}__\s=\s\'(?P<{var_name}>.*)\'".format( + var_name=var_name) + + path = ('msh', '__init__.py',) + + return re.search(regex, read(*path)).group(var_name) + + +def read(*parts): + filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts) + + sys.stdout.write(filename) + + with io.open(filename, encoding='utf-8', mode='rt') as fp: + return fp.read() + + +packages = ['msh'] + + +install_requires = [] + + +classifiers = ['Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.6', + ] + +setup( + name='msh', + version=get_variable('version'), + description=get_variable('description'), + long_description=read('README.md'), + url='https://github.com/msh-contrib/msh', + author='Oleh Kuchuk', + author_email='kuchuklehjs@gmail.com', + license=read('LICENSE.txt'), + packages=packages, + install_requires=install_requires, + zip_safe=False, + classifiers=classifiers, + entry_points={ + 'console_scripts': [ + 'msh=msh.cli:entrypoint' + ], + }, + keywords=[ + 'shell', + 'posix', + 'bash', + ], +) diff --git a/tests/conftest.py b/tests/conftest.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..7031753 --- /dev/null +++ b/tests/test_graph.py @@ -0,0 +1,5 @@ +from msh.graph import Graph + + +def test_graph(): + pass diff --git a/tests/test_node.py b/tests/test_node.py new file mode 100644 index 0000000..3d21801 --- /dev/null +++ b/tests/test_node.py @@ -0,0 +1,30 @@ +import pytest + +from msh.node import Node + + +def test_key(): + node = Node('a') + assert node.key == 'a' + + +def test_fail_on_wrong_type(): + with pytest.raises(TypeError): + Node([]) + + +def test_add_edge(): + a, b = Node('a'), Node('b') + a.add_edge(b) + assert b in a.edges + + +def test_add_edge_wrong_param(): + with pytest.raises(AssertionError): + Node('a').add_edge([]) + + +def test_get_connections(): + a, b = Node('a'), Node('b') + a.add_edge(b) + assert a.get_connections() == [b] diff --git a/tests/test_resolve.py b/tests/test_resolve.py new file mode 100644 index 0000000..959234f --- /dev/null +++ b/tests/test_resolve.py @@ -0,0 +1,21 @@ +from msh.resolve import resolve +from msh.graph import Graph +from msh.node import Node + + +def test_resolve(): + graph = Graph() + graph.add_node('a') + graph.add_node('b') + graph.add_node('c') + graph.add_node('d') + + graph.add_edge('a', 'b') + graph.add_edge('b', 'c') + graph.add_edge('c', 'a') + # graph.add_edge('a', 'c') + # graph.add_edge('a', 'd') + + + assert list(map(lambda node: node.id, resolve( + graph.get_node('a')))) == ['b', 'a']