From 0ae85d50c25697f6845ec057f27eda90178fa37f Mon Sep 17 00:00:00 2001 From: Miller Wilt Date: Tue, 24 Jul 2018 17:38:03 -0400 Subject: [PATCH] Create prototpye crater cli Create a prototype cli tool called crater. This tool is responsible for creating Hipaacrate files, which store all the information for a given Crate. To support this, create the Crate API, and include tests. --- Pipfile | 3 ++ Pipfile.lock | 32 +++++++++++- hipaacrates/__init__.py | 4 ++ hipaacrates/__main__.py | 104 ++++++++++++++++++++++++++++++++++++++ hipaacrates/crate.py | 60 ++++++++++++++++++++++ hipaacrates/version.py | 3 ++ setup.py | 1 + tests/crates_file_test.py | 89 ++++++++++++++++++++++++++++++++ 8 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 hipaacrates/crate.py create mode 100644 hipaacrates/version.py create mode 100644 tests/crates_file_test.py diff --git a/Pipfile b/Pipfile index d08c951..a4491b0 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,9 @@ name = "pypi" typing = "*" typing-extensions = "*" +pyyaml = "*" +click = "*" +filelock = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 81a9fae..9f70092 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9321b785fcff1506be9b9472b899d604e57b6a638e9c40970e586a435f046f63" + "sha256": "38f6570976f29c79517ebb4742294ffdb3804d2f5f7b11b0b4d378a24dce94a8" }, "host-environment-markers": { "implementation_name": "cpython", @@ -27,6 +27,35 @@ ] }, "default": { + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "version": "==6.7" + }, + "filelock": { + "hashes": [ + "sha256:011327d4ed939693a5b28c0fdf2fd9bda1f68614c1d6d0643a89382ce9843a71" + ], + "version": "==3.0.4" + }, + "pyyaml": { + "hashes": [ + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf" + ], + "version": "==3.13" + }, "typing": { "hashes": [ "sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8", @@ -248,6 +277,7 @@ "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa" ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", "version": "==1.1.0" }, "urllib3": { diff --git a/hipaacrates/__init__.py b/hipaacrates/__init__.py index e69de29..eeacd8e 100644 --- a/hipaacrates/__init__.py +++ b/hipaacrates/__init__.py @@ -0,0 +1,4 @@ +from . import crate +from . import version + +__version__ = version.__version__ diff --git a/hipaacrates/__main__.py b/hipaacrates/__main__.py index e69de29..9f16197 100644 --- a/hipaacrates/__main__.py +++ b/hipaacrates/__main__.py @@ -0,0 +1,104 @@ +import hashlib +import os +import tempfile + +import click +from filelock import FileLock, Timeout + +from . import crate +from . import version + +def _get_lock_file_name() -> str: + return os.path.join( + tempfile.gettempdir(), + _hash_cwd(), + ) + +def _hash_cwd() -> str: + return hashlib.sha256(os.getcwdb()).hexdigest() + +CRATE_FILE = "Hipaacrate" +LOCK_FILE = FileLock(_get_lock_file_name(), timeout=0.1) + +@click.group() +@click.version_option(version.__version__, prog_name="crater") +def crater(): + pass + +@crater.command() +@click.argument("bundles", nargs=-1, metavar="BUNDLE [BUNDLE]...") +@click.pass_context +def add(ctx, bundles): + if not bundles: + ctx.fail("Expected one or more Bundle name") + + try: + LOCK_FILE.acquire() + c = crate.read_yaml(CRATE_FILE) + except FileNotFoundError: + ctx.fail("Could not open the Hipaacrate file - have you run 'crater init'?") + except Timeout: + ctx.fail("The Hipaacrate file is currently locked - a separate process must be using it") + else: + combined = set(c.bundles) | set(bundles) + c.bundles = list(combined) + c.bundles.sort() + c.to_yaml(CRATE_FILE) + LOCK_FILE.release() + +@crater.command() +@click.option("-n", "--name", prompt=True, help="Crate name", metavar="NAME") +@click.option("-v", "--version", prompt=True, help="Crate version", metavar="VERSION") +@click.pass_context +def init(ctx, name, version) -> None: + try: + LOCK_FILE.acquire() + except Timeout: + ctx.fail("The Hipaacrate file is currently locked - a separate process must be using it") + else: + c = crate.new(name, version) + c.to_yaml(CRATE_FILE) + LOCK_FILE.release() + +@crater.command() +@click.argument("bundles", nargs=-1, metavar="BUNDLE [BUNDLE]...") +@click.pass_context +def remove(ctx, bundles): + if not bundles: + ctx.fail("Expected one or more Bundle name") + + try: + LOCK_FILE.acquire() + c = crate.read_yaml(CRATE_FILE) + except FileNotFoundError: + ctx.fail("Could not open the Hipaacrate file - have you run 'crater init'?") + except Timeout: + ctx.fail("The Hipaacrate file is currently locked - a separate process must be using it") + else: + to_remove = set(bundles) + existing = set(c.bundles) + if not existing >= to_remove: + ctx.fail("No Bundles named {} added".format(", ".join(to_remove - existing))) + else: + c.bundles = list(existing - to_remove) + c.bundles.sort() + c.to_yaml(CRATE_FILE) + LOCK_FILE.release() + +@crater.command() +@click.argument("value", type=click.Choice(["author", "bundles", "name", "version"])) +@click.pass_context +def show(ctx, value) -> None: + try: + LOCK_FILE.acquire() + c = crate.read_yaml(CRATE_FILE) + except FileNotFoundError: + ctx.fail("Could not open the Hipaacrate file - have you run 'crater init'?") + except Timeout: + ctx.fail("The Hipaacrate file is currently locked - a separate process must be using it") + else: + click.echo(getattr(c, value)) + LOCK_FILE.release() + +if __name__ == '__main__': + crater() diff --git a/hipaacrates/crate.py b/hipaacrates/crate.py new file mode 100644 index 0000000..10e1484 --- /dev/null +++ b/hipaacrates/crate.py @@ -0,0 +1,60 @@ +from typing import Iterable, List + +import yaml + +class Crate(object): + def __init__(self, name: str, version: str, author: str, bundles: List[str]) -> None: + self.author = author + self.bundles = bundles + self.name = name + self.version = version + + def __str__(self) -> str: + return "Crate(name={}, version={}, author={}, bundles={}".format( + self.name, self.version, self.author, self.bundles, + ) + + def to_yaml(self, filepath: str = None) -> str: + yaml_text = yaml.safe_dump(dict( + name=self.name, + author=self.author, + version=self.version, + bundles=self.bundles, + ), default_flow_style=False) + + if filepath is not None: + with open(filepath, "w") as f: + f.write(yaml_text) + + return yaml_text + +def new(name: str, version: str, author: str = None, bundles: Iterable[str] = None) -> Crate: + """ + Create a new Crate + """ + if author is None: + author = "" + if bundles is None: + bundles = [] + else: + bundles = list(bundles) + return Crate(name=name, version=version, author=author, bundles=bundles) + +def parse(text: str) -> Crate: + """ + Parse and load a Crate from a YAML string + """ + parsed = yaml.safe_load(text) + return Crate( + name=parsed["name"], + version=parsed["version"], + author=parsed.get("author"), + bundles=parsed.get("bundles") + ) + +def read_yaml(filepath: str) -> Crate: + """ + Load a Crate from a YAML file + """ + with open(filepath) as f: + return parse(f.read()) diff --git a/hipaacrates/version.py b/hipaacrates/version.py new file mode 100644 index 0000000..67e7156 --- /dev/null +++ b/hipaacrates/version.py @@ -0,0 +1,3 @@ +"""Declare package version.""" + +__version__ = "0.0.1" diff --git a/setup.py b/setup.py index d86b7a2..769c181 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ def find_version(*file_paths): ], entry_points={ "console_scripts": [ + "crater = hipaacrates.__main__:crater" ], }, classifiers=[ diff --git a/tests/crates_file_test.py b/tests/crates_file_test.py new file mode 100644 index 0000000..8e94f0c --- /dev/null +++ b/tests/crates_file_test.py @@ -0,0 +1,89 @@ +import pytest +import yaml + +from hipaacrates import crate + +def test_crate_creation_minimal(): + name = "mycrate" + version = "0.0.1" + + actual = crate.new(name=name, version=version) + assert isinstance(actual, crate.Crate) + + assert actual.name == name + assert actual.version == version + assert actual.bundles == [] + assert actual.author == "" + +def test_crate_creation_with_bundles(): + name = "mycrate" + version = "0.0.1" + bundles = ["mybundle", "foobundle"] + + actual = crate.new(name=name, version=version, bundles=bundles) + assert isinstance(actual, crate.Crate) + + assert actual.name == name + assert actual.version == version + assert actual.bundles == bundles + assert actual.author == "" + +def test_crate_creation_with_author(): + name = "mycrate" + version = "0.0.1" + author = "me" + + actual = crate.new(name=name, version=version, author=author) + assert isinstance(actual, crate.Crate) + + assert actual.name == name + assert actual.version == version + assert actual.bundles == [] + assert actual.author == author + +def test_crate_to_yaml(): + name = "mycrate" + version = "0.0.1" + author = "me" + bundles = ["mybundle", "foobundle"] + + c = crate.new(name=name, version=version, author=author, bundles=bundles) + actual = c.to_yaml() + + items = [s.strip() for s in actual.splitlines()] + assert "name: {}".format(name) in items + assert "version: {}".format(version) in items + assert "author: {}".format(author) in items + assert "bundles:" in items + assert "- mybundle" in items + assert "- foobundle" in items + +@pytest.fixture +def cratetext(): + return """ + name: mycrate + version: 0.0.1 + author: me + bundles: + - mybundle + - foobundle + """ + +def test_crate_parse(cratetext): + c = crate.parse(cratetext) + assert isinstance(c, crate.Crate) + + assert c.name == "mycrate" + assert c.version == "0.0.1" + assert c.author == "me" + assert c.bundles == ["mybundle", "foobundle"] + +def test_read_yaml(tmpdir, cratetext): + p = tmpdir.join("example.yaml") + p.write(cratetext) + + c = crate.read_yaml(str(p)) + assert c.name == "mycrate" + assert c.version == "0.0.1" + assert c.author == "me" + assert c.bundles == ["mybundle", "foobundle"]