From ef1fdbfdde0291f42e2a5abe4fe101571af1faa2 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 28 Feb 2024 16:34:46 -0500 Subject: [PATCH 1/3] initial cli attempt --- poetry.lock | 55 +++++++++++++++++- pyproject.toml | 2 + src/pydantic_zarr/cli.py | 118 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/pydantic_zarr/cli.py diff --git a/poetry.lock b/poetry.lock index f1cae87..bd9bcf8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -467,6 +467,30 @@ importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -536,6 +560,17 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -1322,6 +1357,24 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.2.2" @@ -1514,4 +1567,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "559a98ae01964c1ad957c1f5b4dbaa4da2e5a0043812a33e6cf9f5e38ad6816e" +content-hash = "48ab7d9c6853657a59cf8fe705c84815c07ceba9287b5f09a59b17de91e6ca1c" diff --git a/pyproject.toml b/pyproject.toml index 1f3068f..ec34893 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ python = "^3.9" zarr = "^2.14.2" pydantic = "^2.0.0" typing-extensions = {version = "^4.7.1", python = "<3.12"} +click = "^8.1.7" +rich = "^13.7.1" [tool.poetry.group.dev.dependencies] pytest = "^7.3.1" diff --git a/src/pydantic_zarr/cli.py b/src/pydantic_zarr/cli.py new file mode 100644 index 0000000..66220c3 --- /dev/null +++ b/src/pydantic_zarr/cli.py @@ -0,0 +1,118 @@ +from typing import Any, Literal, Tuple, Union, cast +import click +from pydantic_zarr.v2 import ArraySpec, GroupSpec, from_zarr +from pydantic import ValidationError +import zarr +from rich.console import Console +from rich.traceback import install +from rich import print_json +from importlib import import_module + +install() + + +@click.group +def cli(): + pass + + +def validate( + cls: GroupSpec | ArraySpec, node: zarr.Array | zarr.Group, console: Console +): + """ + Attempt to validate a Zarr array or group against a `GroupSpec` or `ArraySpec. + The result of the validation (success or failure) is printed to the terminal. + """ + class_name = f"{cls.__module__}.{cls.__name__}" + protocol = "file" + if hasattr(node.store, "fs"): + if isinstance(node.store.fs.protocol, tuple): + protocol = node.store.fs.protocol[0] + node_path = protocol + "://" + node.store.path + "/" + node.path + try: + cls.from_zarr(node) + console.print( + f"[bold]{class_name}[/bold]: [blue]{node_path}[/blue] validated " + "successfully." + ) + except ValidationError as e: + console.print( + f"[bold]{class_name}[/bold]: [blue]{node_path}[/blue] failed " + "validation due to the following validation errors:" + ) + console.print(str(e)) + + +@cli.command +@click.argument("url", type=click.STRING) +@click.argument("group_type", type=click.STRING) +def check(url: str, group_type: str): + """ + Check whether a Zarr / N5 hierarchy, specified by a url, is consistent with a model, + specified by a class name. + + Parameters\n + ---------- + + url: A url referring to a Zarr / N5 group or dataset. + + group_type: A class name for one of the models defined in `cellmap-schemas`. + """ + store_stem, prefix, component_path = parse_url(url) + store = guess_store(store_stem=store_stem, prefix=prefix) + node = zarr.open(store, path=component_path, mode="r") + + console = Console() + + *mod_path_parts, classname = group_type.split(".") + mod = import_module(".".join([__package__] + mod_path_parts)) + cls: Any = getattr(mod, classname) + if not issubclass(cls, (ArraySpec, GroupSpec)): + raise ValueError( + f"Object of type {type(cls)} is not a `GroupSpec` or `ArraySpec` and thus it does not support validation with this command." + ) + cls = cast(Union[ArraySpec, GroupSpec], cls) + validate(cls, node, console) + + +@cli.command +@click.option("--url", type=click.STRING) +@click.option("--store", type=click.STRING) +@click.option("--path", type=click.STRING) +def inspect(url: str): + store_stem, prefix, component_path = parse_url(url) + store = guess_store(store_stem=store_stem, prefix=prefix) + node = zarr.open(store, path=component_path, mode="r") + print_json(from_zarr(node).model_dump_json(indent=2)) + + +def guess_store(store_stem: str, prefix: Literal[".zarr"]) -> zarr.storage.BaseStore: + store = zarr.storage.FSStore(store_stem + prefix) + return store + + +def parse_url(url: str) -> Tuple[str, Literal[".zarr"], str]: + """ + Parses a string into a pre-suffix component, a suffix, and a post-suffix component, for the + suffix ".zarr" + + Parameters + ---------- + + url: str + An FSSpec-compatible URL, e.g. s3://bucket/path.zarr/foo or http://domain.com/path.n5/bar + + Returns + ------- + Tuple[str, Literal['.zarr'], str] + """ + + # try zarr + if ".zarr" not in url: + raise ValueError( + f"Invalid url parameter. Got {url}, but expected a string containing a at " + "least one instance of '.zarr'. Are you sure this url refers to " + "Zarr" + ) + + return url.partition(".zarr") From fa2c2605d688ea61dbdf054380b4623bedfe62bc Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 28 Feb 2024 16:35:25 -0500 Subject: [PATCH 2/3] fix repo name --- mkdocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yaml b/mkdocs.yaml index c7be668..146814b 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -5,7 +5,7 @@ site_description: >- Documentation for pydantic-zarr # Repository -repo_name: d-v-b/pydantic-zarr +repo_name: janelia-cellmap/pydantic-zarr repo_url: https://github.com/janelia-cellmap/pydantic-zarr # Copyright From db20b56de55965e9430e07aa91e9083dc221687b Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 28 Feb 2024 16:36:50 -0500 Subject: [PATCH 3/3] Revert "initial cli attempt" This reverts commit ef1fdbfdde0291f42e2a5abe4fe101571af1faa2. --- poetry.lock | 55 +----------------- pyproject.toml | 2 - src/pydantic_zarr/cli.py | 118 --------------------------------------- 3 files changed, 1 insertion(+), 174 deletions(-) delete mode 100644 src/pydantic_zarr/cli.py diff --git a/poetry.lock b/poetry.lock index bd9bcf8..f1cae87 100644 --- a/poetry.lock +++ b/poetry.lock @@ -467,30 +467,6 @@ importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "2.1.5" @@ -560,17 +536,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "mergedeep" version = "1.3.4" @@ -1357,24 +1322,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "ruff" version = "0.2.2" @@ -1567,4 +1514,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "48ab7d9c6853657a59cf8fe705c84815c07ceba9287b5f09a59b17de91e6ca1c" +content-hash = "559a98ae01964c1ad957c1f5b4dbaa4da2e5a0043812a33e6cf9f5e38ad6816e" diff --git a/pyproject.toml b/pyproject.toml index ec34893..1f3068f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,6 @@ python = "^3.9" zarr = "^2.14.2" pydantic = "^2.0.0" typing-extensions = {version = "^4.7.1", python = "<3.12"} -click = "^8.1.7" -rich = "^13.7.1" [tool.poetry.group.dev.dependencies] pytest = "^7.3.1" diff --git a/src/pydantic_zarr/cli.py b/src/pydantic_zarr/cli.py deleted file mode 100644 index 66220c3..0000000 --- a/src/pydantic_zarr/cli.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Any, Literal, Tuple, Union, cast -import click -from pydantic_zarr.v2 import ArraySpec, GroupSpec, from_zarr -from pydantic import ValidationError -import zarr -from rich.console import Console -from rich.traceback import install -from rich import print_json -from importlib import import_module - -install() - - -@click.group -def cli(): - pass - - -def validate( - cls: GroupSpec | ArraySpec, node: zarr.Array | zarr.Group, console: Console -): - """ - Attempt to validate a Zarr array or group against a `GroupSpec` or `ArraySpec. - The result of the validation (success or failure) is printed to the terminal. - """ - class_name = f"{cls.__module__}.{cls.__name__}" - protocol = "file" - if hasattr(node.store, "fs"): - if isinstance(node.store.fs.protocol, tuple): - protocol = node.store.fs.protocol[0] - node_path = protocol + "://" + node.store.path + "/" + node.path - try: - cls.from_zarr(node) - console.print( - f"[bold]{class_name}[/bold]: [blue]{node_path}[/blue] validated " - "successfully." - ) - except ValidationError as e: - console.print( - f"[bold]{class_name}[/bold]: [blue]{node_path}[/blue] failed " - "validation due to the following validation errors:" - ) - console.print(str(e)) - - -@cli.command -@click.argument("url", type=click.STRING) -@click.argument("group_type", type=click.STRING) -def check(url: str, group_type: str): - """ - Check whether a Zarr / N5 hierarchy, specified by a url, is consistent with a model, - specified by a class name. - - Parameters\n - ---------- - - url: A url referring to a Zarr / N5 group or dataset. - - group_type: A class name for one of the models defined in `cellmap-schemas`. - """ - store_stem, prefix, component_path = parse_url(url) - store = guess_store(store_stem=store_stem, prefix=prefix) - node = zarr.open(store, path=component_path, mode="r") - - console = Console() - - *mod_path_parts, classname = group_type.split(".") - mod = import_module(".".join([__package__] + mod_path_parts)) - cls: Any = getattr(mod, classname) - if not issubclass(cls, (ArraySpec, GroupSpec)): - raise ValueError( - f"Object of type {type(cls)} is not a `GroupSpec` or `ArraySpec` and thus it does not support validation with this command." - ) - cls = cast(Union[ArraySpec, GroupSpec], cls) - validate(cls, node, console) - - -@cli.command -@click.option("--url", type=click.STRING) -@click.option("--store", type=click.STRING) -@click.option("--path", type=click.STRING) -def inspect(url: str): - store_stem, prefix, component_path = parse_url(url) - store = guess_store(store_stem=store_stem, prefix=prefix) - node = zarr.open(store, path=component_path, mode="r") - print_json(from_zarr(node).model_dump_json(indent=2)) - - -def guess_store(store_stem: str, prefix: Literal[".zarr"]) -> zarr.storage.BaseStore: - store = zarr.storage.FSStore(store_stem + prefix) - return store - - -def parse_url(url: str) -> Tuple[str, Literal[".zarr"], str]: - """ - Parses a string into a pre-suffix component, a suffix, and a post-suffix component, for the - suffix ".zarr" - - Parameters - ---------- - - url: str - An FSSpec-compatible URL, e.g. s3://bucket/path.zarr/foo or http://domain.com/path.n5/bar - - Returns - ------- - Tuple[str, Literal['.zarr'], str] - """ - - # try zarr - if ".zarr" not in url: - raise ValueError( - f"Invalid url parameter. Got {url}, but expected a string containing a at " - "least one instance of '.zarr'. Are you sure this url refers to " - "Zarr" - ) - - return url.partition(".zarr")