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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## v0.2.1 - 2026-07-15

### Features

- `list` commands (`cube`, `dimension`, `process`, `subset`, `view`) now print each
item as a `-` bullet by default, via a new shared `generic_list()` helper.
- `--output-raw` now also applies to `list` commands, printing the old plain,
one-item-per-line output.

### Breaking changes

- Default stdout of `tm1cli <resource> list` (and its `ls` alias) is no longer plain
one-item-per-line output. Scripts relying on that must add `--output-raw`.

## v0.2.0 - 2026-07-15

### Features
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,14 @@ tm1cli --help

### Output formatting

By default, `exists` commands print a human-readable result, e.g. `✅ Cube exists!`.
Pass the global `--output-raw` flag before the command to get the raw `True`/`False`
value instead, e.g. for scripting:
By default, `exists` commands print a human-readable result, e.g. `✅ Cube exists!`,
and `list` commands print each item as a `-` bullet, e.g. `- Cube1`. Pass the global
`--output-raw` flag before the command to get the raw `True`/`False` or plain,
one-item-per-line output instead, e.g. for scripting:

```bash
tm1cli --output-raw cube exists <cube_name>
tm1cli --output-raw cube list
```

### Configuration
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "tm1cli"
version = "0.2.0"
version = "0.2.1"
description = "A command-line interface (CLI) tool for interacting with TM1 servers using TM1py."
authors = ["onefloid <[email protected]>"]
license = "MIT License"
Expand Down
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class MockedCubeService:
cubes = ["Cube1", "Cube2"]

def get_all_names(self, cube_name: str):
def get_all_names(self, skip_control_cubes: bool):
return self.cubes

def exists(self, cube_name: str):
Expand Down Expand Up @@ -46,6 +46,9 @@ def exists(self, dimension_name: str, subset_name: str, private: bool):


class MockedProcessService:
def get_all_names(self):
return ["Process1", "Process2"]

def exists(self, name: str):
return False if "not" in name else True

Expand Down
14 changes: 10 additions & 4 deletions tests/test_cmd_cubes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@


@pytest.mark.parametrize("command", ["list", "ls"])
def test_cube_list(mocker, command):
mocker.patch("tm1cli.commands.cube.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["cube", command])
@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, "- Cube1\n- Cube2\n"), ("--output-raw", "Cube1\nCube2\n")],
)
def test_cube_list(mocker, command, raw_option, expected_output):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
args = [raw_option] if raw_option else []
args += ["cube", command]
result = runner.invoke(app, args)
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "Cube1\nCube2\n"
assert result.stdout == expected_output


@pytest.mark.parametrize(
Expand Down
17 changes: 13 additions & 4 deletions tests/test_cmd_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@


@pytest.mark.parametrize("command", ["list", "ls"])
def test_dimension_list(mocker, command):
mocker.patch("tm1cli.commands.dimension.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["dimension", command])
@pytest.mark.parametrize(
"raw_option,expected_output",
[
(None, "- Dimension1\n- Dimension2\n- Dimension3\n"),
("--output-raw", "Dimension1\nDimension2\nDimension3\n"),
],
)
def test_dimension_list(mocker, command, raw_option, expected_output):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
args = [raw_option] if raw_option else []
args += ["dimension", command]
result = runner.invoke(app, args)
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "Dimension1\nDimension2\nDimension3\n"
assert result.stdout == expected_output


@pytest.mark.parametrize(
Expand Down
18 changes: 18 additions & 0 deletions tests/test_cmd_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@
runner = CliRunner()


@pytest.mark.parametrize("command", ["list", "ls"])
@pytest.mark.parametrize(
"raw_option,expected_output",
[
(None, "- Process1\n- Process2\n"),
("--output-raw", "Process1\nProcess2\n"),
],
)
def test_process_list(mocker, command, raw_option, expected_output):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
args = [raw_option] if raw_option else []
args += ["process", command]
result = runner.invoke(app, args)
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == expected_output


@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, "✅ Process exists!\n"), ("--output-raw", "True\n")],
Expand Down
17 changes: 13 additions & 4 deletions tests/test_cmd_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@


@pytest.mark.parametrize("command", ["list", "ls"])
def test_subset_list(mocker, command):
mocker.patch("tm1cli.commands.subset.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["subset", command, "Dimension1"])
@pytest.mark.parametrize(
"raw_option,expected_output",
[
(None, "- Subset1\n- Subset2\n- Subset3\n"),
("--output-raw", "Subset1\nSubset2\nSubset3\n"),
],
)
def test_subset_list(mocker, command, raw_option, expected_output):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
args = [raw_option] if raw_option else []
args += ["subset", command, "Dimension1"]
result = runner.invoke(app, args)
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "Subset1\nSubset2\nSubset3\n"
assert result.stdout == expected_output


@pytest.mark.parametrize(
Expand Down
17 changes: 13 additions & 4 deletions tests/test_cmd_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ def test_view_exists(mocker, view_name, private_flag, exists_result, raw_option)
assert result.stdout == f"{icon} View {word}!\n"


def test_view_list(mocker):
mocker.patch("tm1cli.commands.view.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["view", "list", "example_cube"])
@pytest.mark.parametrize(
"raw_option,expected_output",
[
(None, "- View1\n- View2\n- View3\n"),
("--output-raw", "View1\nView2\nView3\n"),
],
)
def test_view_list(mocker, raw_option, expected_output):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
args = [raw_option] if raw_option else []
args += ["view", "list", "example_cube"]
result = runner.invoke(app, args)

assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "View1\nView2\nView3\n"
assert result.stdout == expected_output
9 changes: 2 additions & 7 deletions tm1cli/commands/cube.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from typing import Annotated

import typer
from rich import print # pylint: disable=redefined-builtin
from TM1py.Services import TM1Service

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.various import resolve_database
from tm1cli.utils.generic import execute_exists, generic_list
from tm1cli.utils.watch import watch_option

app = typer.Typer()
Expand All @@ -30,9 +27,7 @@ def list_cube(
List cubes
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for cube in tm1.cubes.get_all_names(skip_control_cubes):
print(cube)
generic_list("cubes", ctx, database, skip_control_cubes=skip_control_cubes)


@app.command()
Expand Down
9 changes: 2 additions & 7 deletions tm1cli/commands/dimension.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from typing import Annotated

import typer
from rich import print # pylint: disable=redefined-builtin
from TM1py.Services import TM1Service

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.various import resolve_database
from tm1cli.utils.generic import execute_exists, generic_list
from tm1cli.utils.watch import watch_option

app = typer.Typer()
Expand All @@ -30,9 +27,7 @@ def list_dimension(
List dimensions
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for dim in tm1.dimensions.get_all_names(skip_control_dims):
print(dim)
generic_list("dimensions", ctx, database, skip_control_dims=skip_control_dims)


@app.command()
Expand Down
6 changes: 2 additions & 4 deletions tm1cli/commands/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from TM1py.Services import TM1Service

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.generic import execute_exists, generic_list
from tm1cli.utils.tm1yaml import dump_process, load_process
from tm1cli.utils.various import print_error_and_exit, resolve_database
from tm1cli.utils.watch import watch_option
Expand All @@ -34,9 +34,7 @@ def list_process(
List processes
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for process in tm1.processes.get_all_names():
print(process)
generic_list("processes", ctx, database)


@app.command()
Expand Down
9 changes: 2 additions & 7 deletions tm1cli/commands/subset.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from typing import Annotated

import typer
from rich import print # pylint: disable=redefined-builtin
from TM1py.Services import TM1Service

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.various import resolve_database
from tm1cli.utils.generic import execute_exists, generic_list
from tm1cli.utils.watch import watch_option

app = typer.Typer()
Expand All @@ -24,9 +21,7 @@ def list_subset(
List subsets
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for subset in tm1.subsets.get_all_names(dimension_name):
print(subset)
generic_list("subsets", ctx, database, dimension_name=dimension_name)


@app.command()
Expand Down
9 changes: 2 additions & 7 deletions tm1cli/commands/view.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from typing import Annotated

import typer
from rich import print # pylint: disable=redefined-builtin
from TM1py.Services import TM1Service

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.various import resolve_database
from tm1cli.utils.generic import execute_exists, generic_list
from tm1cli.utils.watch import watch_option

app = typer.Typer()
Expand All @@ -23,9 +20,7 @@ def list_view(
List views
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for view in tm1.views.get_all_names(cube_name):
print(view)
generic_list("views", ctx, database, cube_name=cube_name)


@app.command()
Expand Down
28 changes: 28 additions & 0 deletions tm1cli/utils/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,31 @@ def execute_exists(attribute_name, ctx, database, **args):
print(f":white_check_mark: {output_name} exists!")
else:
print(f":x: {output_name} does not exist!")


def generic_list(attribute_name, ctx, database, **args):
"""
Util function to execute a list function
"""

database_config = resolve_database(ctx, database)

try:
with TM1Service(**database_config) as tm1:
attribute = getattr(tm1, attribute_name)
output = attribute.get_all_names(**args)
except (AttributeError, TypeError):
# Programming errors (wrong attribute_name or mismatched kwargs),
# not TM1 runtime errors - let them raise as real tracebacks.
raise
except Exception as e: # pylint: disable=broad-except
print(f"[bold red]{type(e).__name__}:[/bold red] {e}")
raise typer.Exit(code=1) from e

if ctx.obj["raw"]:
for item in output:
print(item)
return

for item in output:
print(f"- {item}")
Loading