diff --git a/django_utils_lib/cli.py b/django_utils_lib/cli.py new file mode 100644 index 0000000..3962bac --- /dev/null +++ b/django_utils_lib/cli.py @@ -0,0 +1,25 @@ +from pathlib import Path +from typing import List + +import typer +from rich.console import Console + +from django_utils_lib.commands import generate_combined_spdx_sbom_json as generate_combined_spdx_sbom_json_cmd + +app = typer.Typer() +console = Console() + + +@app.command() +def generate_combined_spdx_sbom_json( + sbom_paths: List[str], + out_path: Path, + merged_name="Combined SBOM", + merged_namespace="https//localhost", +): + out_json = generate_combined_spdx_sbom_json_cmd(sbom_paths, merged_name, merged_namespace) + out_path.write_text(out_json) + + +if __name__ == "__main__": + app() diff --git a/django_utils_lib/cli_utils.py b/django_utils_lib/cli_utils.py new file mode 100644 index 0000000..7c81699 --- /dev/null +++ b/django_utils_lib/cli_utils.py @@ -0,0 +1,25 @@ +from typing import Any + +import rich.markup as rich_markup +from rich.console import Console + + +class AlwaysEscapeMarkupConsole(Console): + """ + Wrapper around Rich's Console to force logging to alway use markup AND escape output + + Note: It would be nice to be able to use `console.push_render_hook` to implement + escaping through a custom renderer hook, rather than having to subclass the + entire `Console` class, but the render hook appears to come too late in the + processing chain to be able to auto-escape. + """ + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self._markup = True + + def log(self, *objects: Any, **kwargs: Any) -> None: + return super().log(*[rich_markup.escape(o) for o in objects], **kwargs) + + def print(self, *objects: Any, **kwargs: Any) -> None: + return super().print(*[rich_markup.escape(o) for o in objects], **kwargs) diff --git a/django_utils_lib/commands.py b/django_utils_lib/commands.py new file mode 100644 index 0000000..21b623f --- /dev/null +++ b/django_utils_lib/commands.py @@ -0,0 +1,30 @@ +import json +from pathlib import Path +from typing import Dict, List + + +def generate_combined_spdx_sbom_json( + sbom_paths: List[str], + merged_name="Combined SBOM", + merged_namespace="https//localhost", +) -> str: + """ + Combine multiple SPDX formatted JSON SBOMs, into a single JSON file + + Warning: This is a basic implementation that makes some assumptions about the + validity of the input files and what sort of output is desired. + """ + with open(sbom_paths[0], "r") as file: + combined_sbom_json: Dict = json.load(file) + expected_spdx_version = combined_sbom_json["spdxVersion"] + combined_sbom_json["name"] = merged_name + combined_sbom_json["documentNamespace"] = merged_namespace + + for sbom_path in sbom_paths: + sbom_json = json.loads(Path(sbom_path).read_text()) + # Don't allow combining outputs with different versions + assert sbom_json["spdxVersion"] == expected_spdx_version + for sbom_key in ["files", "packages", "relationships"]: + combined_sbom_json[sbom_key] += sbom_json[sbom_key] + + return json.dumps(combined_sbom_json, indent=2) diff --git a/poetry.lock b/poetry.lock index 9002d57..f5b9c93 100644 --- a/poetry.lock +++ b/poetry.lock @@ -127,6 +127,20 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -366,6 +380,41 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[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 = "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 = "mypy" version = "1.11.2" @@ -487,6 +536,20 @@ typing-extensions = "*" [package.extras] dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "8.3.3" @@ -620,6 +683,25 @@ 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.9.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.1-py3-none-any.whl", hash = "sha256:b340e739f30aa58921dc477b8adaa9ecdb7cecc217be01d93730ee1bc8aa83be"}, + {file = "rich-13.9.1.tar.gz", hash = "sha256:097cffdf85db1babe30cc7deba5ab3a29e1b9885047dab24c57e9a7f8a9c1466"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.6.4" @@ -647,6 +729,17 @@ files = [ {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, ] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + [[package]] name = "sqlparse" version = "0.5.1" @@ -684,6 +777,23 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typer" +version = "0.12.5" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + [[package]] name = "types-pyyaml" version = "6.0.12.20240808" @@ -737,4 +847,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "fc1d2da4286af57c2854a14eb3253405e9d0d8f6f7f3a3af5dfcf16b16503d46" +content-hash = "e64e4090145b72894f29316b7449297556e711a0132e84155e88d0d1cfc397dd" diff --git a/pyproject.toml b/pyproject.toml index 0e21d41..218aceb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,12 @@ description = "A utility library for working with Django" authors = ["Innolitics"] readme = "README.md" +[tool.poetry.scripts] +dul-cli = "django_utils_lib.cli:app" + [tool.poetry.dependencies] python = "^3.9" +typer = "^0.12.5" [tool.poetry.group.dev.dependencies] @@ -39,6 +43,7 @@ testpaths = [ ] pythonpath = ". django_utils_lib" auto_debug = true +auto_debug_wait_for_connect = false [build-system] requires = ["poetry-core"]