Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 12 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,10 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: install-poetry
uses: snok/install-poetry@v1
with:
version: 1.4.0
virtualenvs-in-project: false
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
run: |
python -m pip install "poetry~=2.2.1"
poetry install --all-extras
- name: run isort and black
run: |
poetry run isort . --check
Expand All @@ -54,14 +50,10 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: install-poetry
uses: snok/install-poetry@v1
with:
version: 1.4.0
virtualenvs-in-project: false
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
run: |
python -m pip install "poetry~=2.2.1"
poetry install --all-extras
- name: install pytest-xdist for parallel tests
run: poetry run pip install pytest-xdist
- name: lint
Expand Down Expand Up @@ -95,14 +87,10 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: install-poetry
uses: snok/install-poetry@v1
with:
version: 1.4.0
virtualenvs-in-project: false
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
run: |
python -m pip install "poetry~=2.2.1"
poetry install --all-extras
- name: install pytest-xdist for parallel tests
run: poetry run pip install pytest-xdist
- name: integration tests
Expand Down Expand Up @@ -135,14 +123,10 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: install-poetry
uses: snok/install-poetry@v1
with:
version: 1.4.0
virtualenvs-in-project: false
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
run: |
python -m pip install "poetry~=2.2.1"
poetry install --all-extras
- name: install pytest-xdist for parallel tests
run: poetry run pip install pytest-xdist
- name: library tests
Expand Down
25 changes: 25 additions & 0 deletions buildingmotif/bin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
from pathlib import Path

from buildingmotif import BuildingMOTIF
from buildingmotif.bin.library import add_commands
from buildingmotif.dataclasses import Library
from buildingmotif.ingresses.bacnet import BACnetNetwork

cli = argparse.ArgumentParser(
prog="buildingmotif", description="CLI Interface for common BuildingMOTIF tasks"
)
subparsers = cli.add_subparsers(dest="subcommand")

subcommands = {}
log = logging.getLogger()
log.setLevel(logging.INFO)
Expand All @@ -39,6 +41,22 @@ def decorator(func):
return decorator


def subparser(*subparser_args, parent=subparsers):
"""Decorates a function and makes it available as a subparser"""

def decorator(func):
subcommand(*subparser_args, parent=parent)(func)

subparser = subcommands[func].add_subparsers(dest=f"{func.__name__}_subcommand")

def subcommand_decorator(*subparser_args, parent=subparser):
return subcommand(*subparser_args, parent=parent)

return subcommand_decorator

return decorator


def get_db_uri(args) -> str:
"""
Fetches the db uri from args, or prints the usage
Expand Down Expand Up @@ -163,6 +181,13 @@ def scan(args):
bacnet_network.dump(Path(args.output_file))


@subparser()
def library(args):
"""A collection of commands for working with libraries"""


add_commands(library)

# entrypoint is actually defined in pyproject.toml; this is here for convenience/testing
if __name__ == "__main__":
app()
158 changes: 158 additions & 0 deletions buildingmotif/bin/cli_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from enum import Enum
from typing import Union

from pygit2.config import Config


class Color(Enum):
"""ANSI color codes for terminal text formatting.

Usage:
Color.GREEN("Example text"), produces green text.
Color.GREEN + "Example text" + Color.RESET, is the same as Color.GREEN("Example text")
"""

BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
LIGHT_GRAY = "\033[37m"
GRAY = "\033[90m"
LIGHT_RED = "\033[91m"
LIGHT_GREEN = "\033[92m"
LIGHT_YELLOW = "\033[93m"
LIGHT_BLUE = "\033[94m"
LIGHT_MAGENTA = "\033[95m"
LIGHT_CYAN = "\033[96m"
WHITE = "\033[97m"
RESET = "\033[0m"

def __call__(self, text):
return f"{self.value}{text}{Color.RESET.value}"

def __str__(self):
return self.value


def print_tree(
tree: dict[Union[str, tuple[str, str]], Union[dict, None]], indent=4
) -> None:
"""Print a tree like dict to the console."""

def _tree_to_str(
item: Union[str, tuple[str, str]],
tree: Union[dict, None] = None,
level: int = 0,
indent: int = 4,
) -> str:
""""""
description = ""
if isinstance(item, tuple):
name, description = item
else:
name = item

if level % 5 == 0:
name = Color.BLUE(name)
elif level % 5 == 1:
name = Color.MAGENTA(name)
elif level % 5 == 2:
name = Color.GREEN(name)
elif level % 5 == 3:
name = Color.CYAN(name)
elif level % 5 == 4:
name = Color.RED(name)

title = f"{name} {description}".strip()

lines = [title]
if tree is None:
return title
for index, (subitem, subtree) in enumerate(tree.items()):
subtree_lines = _tree_to_str(subitem, subtree, level + 1, indent).split(
"\n"
)
last = index == len(tree) - 1

for line_index, line in enumerate(subtree_lines):
prefix = " " * indent
if last:
if line_index == 0:
prefix = f"└{'─' * (indent - 2)} "
else:
if line_index == 0:
prefix = f"├{'─' * (indent - 2)} "
else:
prefix = f"│{' ' * (indent - 2)} "
subtree_lines[line_index] = f"{prefix}{line}"
lines.extend(subtree_lines)
return "\n".join(lines)

lines = []
for subitem, subtree in tree.items():
lines.append(_tree_to_str(subitem, subtree, 0, indent))
print("\n".join(lines))


def get_input(
prompt: str,
default: Union[str, None] = None,
optional: bool = False,
input_type: Union[type] = str,
) -> str | int | float | bool | None:
"""
Helper function to get input from the user with a prompt.
If default is provided, it will be used if the user just presses Enter.
If optional is False, the user must provide an input.
"""
parenthetical = f" [{Color.BLUE(default)}]" if default is not None else ""
if optional and default is not None:
parenthetical = f" [{Color.BLUE(default)}, {Color.MAGENTA('n to skip')}]"

if input_type is bool:
parenthetical = f"{parenthetical} {Color.MAGENTA('(y/n)')}"

while True:
user_input = input(f"{Color.GREEN(prompt)}{parenthetical}: ")
if not user_input:
if default is not None:
user_input = default
elif optional:
return None
else:
print("This field is required. Please provide a value.")
continue
try:
if user_input == "n" and optional:
return None
if input_type in [int, float, str]:
return input_type(user_input)
elif input_type is bool:
true_input = user_input.lower() in ["true", "1", "yes", "y"]
false_input = user_input.lower() in ["false", "0", "no", "n"]
if true_input:
return True
elif false_input:
return False
raise ValueError(f"Invalid input for boolean: {user_input}")
return input_type(user_input)
except ValueError:
print(
f"{Color.RED}Invalid input. Please enter a valid {input_type.__name__}.{Color.RESET}"
)


def git_global_config() -> dict[str, str]:
"""
Fetches the global git configuration.
"""
config = Config.get_global_config()
return {value.name: value.value for value in config}


def arg(*argnames, **kwargs):
"""Helper for defining arguments on subcommands"""
return argnames, kwargs
Loading