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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ tm1cli subset list <dimension_name>
tm1cli subset exists <dimension_name> <subset_name>
```

All list commands support case-insensitive glob filtering. Quote patterns to prevent
your shell from expanding them:

```bash
tm1cli cube list --filter 'Sales*'
tm1cli process list --filter '*Import*'
```

### All Available Commands

Run the following to see all available commands:
Expand Down
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def exists(self, cube_name: str):
class MockedViewService:

def get_all_names(self, cube_name: str):
return ["View1", "View2", "View3"]
return ([], ["View1", "View2", "View3"])

def exists(self, cube_name: str, view_name: str, private: bool):
if "not" in view_name.lower():
Expand Down Expand Up @@ -46,6 +46,11 @@ def exists(self, dimension_name: str, subset_name: str, private: bool):


class MockedProcessService:
processes = ["Process1", "Process2"]

def get_all_names(self):
return self.processes

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

Expand Down
7 changes: 7 additions & 0 deletions tests/test_cmd_cubes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ def test_cube_list(mocker, command):
assert result.stdout == "Cube1\nCube2\n"


def test_cube_list_filter(mocker):
mocker.patch("tm1cli.commands.cube.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["cube", "list", "--filter", "*2"])
assert result.exit_code == 0
assert result.stdout == "Cube2\n"


@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, "✅ Cube exists!\n"), ("--output-raw", "True\n")],
Expand Down
7 changes: 7 additions & 0 deletions tests/test_cmd_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ def test_dimension_list(mocker, command):
assert result.stdout == "Dimension1\nDimension2\nDimension3\n"


def test_dimension_list_filter(mocker):
mocker.patch("tm1cli.commands.dimension.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["dimension", "list", "--filter", "*2"])
assert result.exit_code == 0
assert result.stdout == "Dimension2\n"


@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, "✅ Dimension exists!\n"), ("--output-raw", "True\n")],
Expand Down
7 changes: 7 additions & 0 deletions tests/test_cmd_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
runner = CliRunner()


def test_process_list_filter(mocker):
mocker.patch("tm1cli.commands.process.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["process", "list", "--filter", "*2"])
assert result.exit_code == 0
assert result.stdout == "Process2\n"


@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, "✅ Process exists!\n"), ("--output-raw", "True\n")],
Expand Down
9 changes: 9 additions & 0 deletions tests/test_cmd_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ def test_subset_list(mocker, command):
assert result.stdout == "Subset1\nSubset2\nSubset3\n"


def test_subset_list_filter(mocker):
mocker.patch("tm1cli.commands.subset.TM1Service", MockedTM1Service)
result = runner.invoke(
app, ["subset", "list", "Dimension1", "--filter", "*2"]
)
assert result.exit_code == 0
assert result.stdout == "Subset2\n"


@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, "✅ Subset exists!\n"), ("--output-raw", "True\n")],
Expand Down
9 changes: 9 additions & 0 deletions tests/test_cmd_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,12 @@ def test_view_list(mocker):
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "View1\nView2\nView3\n"


def test_view_list_filter(mocker):
mocker.patch("tm1cli.commands.view.TM1Service", MockedTM1Service)
result = runner.invoke(
app, ["view", "list", "example_cube", "--filter", "*2"]
)
assert result.exit_code == 0
assert result.stdout == "View2\n"
19 changes: 19 additions & 0 deletions tests/test_list_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from tm1cli.utils.list_utils import apply_filter


def test_apply_filter_uses_case_insensitive_globs():
names = ["Sales", "Sales_Budget", "Archive_Sales", "Costs"]

assert apply_filter(names, "sales*") == ["Sales", "Sales_Budget"]
assert apply_filter(names, "*SALES*") == [
"Sales",
"Sales_Budget",
"Archive_Sales",
]


def test_apply_filter_without_wildcards_matches_exactly():
names = ["Sales", "Sales_Budget"]

assert apply_filter(names, "sales") == ["Sales"]
assert apply_filter(names, None) == names
12 changes: 10 additions & 2 deletions tm1cli/commands/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
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.cli_param import (
DATABASE_OPTION,
FILTER_OPTION,
INTERVAL_OPTION,
WATCH_OPTION,
)
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.list_utils import apply_filter
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -17,6 +23,7 @@
def list_cube(
ctx: typer.Context,
database: Annotated[str, DATABASE_OPTION] = None,
name_filter: Annotated[str, FILTER_OPTION] = None,
skip_control_cubes: Annotated[
bool,
typer.Option(
Expand All @@ -31,7 +38,8 @@ def list_cube(
"""

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


Expand Down
12 changes: 10 additions & 2 deletions tm1cli/commands/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
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.cli_param import (
DATABASE_OPTION,
FILTER_OPTION,
INTERVAL_OPTION,
WATCH_OPTION,
)
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.list_utils import apply_filter
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -17,6 +23,7 @@
def list_dimension(
ctx: typer.Context,
database: Annotated[str, DATABASE_OPTION] = None,
name_filter: Annotated[str, FILTER_OPTION] = None,
skip_control_dims: Annotated[
bool,
typer.Option(
Expand All @@ -31,7 +38,8 @@ def list_dimension(
"""

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


Expand Down
12 changes: 10 additions & 2 deletions tm1cli/commands/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
from TM1py.Objects import Process
from TM1py.Services import TM1Service

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.cli_param import (
DATABASE_OPTION,
FILTER_OPTION,
INTERVAL_OPTION,
WATCH_OPTION,
)
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.list_utils import apply_filter
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 @@ -29,13 +35,15 @@ def _get_process(name: str, database_config: dict) -> Process:
def list_process(
ctx: typer.Context,
database: Annotated[str, DATABASE_OPTION] = None,
name_filter: Annotated[str, FILTER_OPTION] = None,
):
"""
List processes
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for process in tm1.processes.get_all_names():
names = tm1.processes.get_all_names()
for process in apply_filter(names, name_filter):
print(process)


Expand Down
12 changes: 10 additions & 2 deletions tm1cli/commands/subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
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.cli_param import (
DATABASE_OPTION,
FILTER_OPTION,
INTERVAL_OPTION,
WATCH_OPTION,
)
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.list_utils import apply_filter
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -19,13 +25,15 @@ def list_subset(
dimension_name: str,
# hierarchy_name: str = None,
database: Annotated[str, DATABASE_OPTION] = None,
name_filter: Annotated[str, FILTER_OPTION] = None,
):
"""
List subsets
"""

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


Expand Down
12 changes: 10 additions & 2 deletions tm1cli/commands/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
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.cli_param import (
DATABASE_OPTION,
FILTER_OPTION,
INTERVAL_OPTION,
WATCH_OPTION,
)
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.list_utils import apply_filter
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -18,13 +24,15 @@ def list_view(
ctx: typer.Context,
cube_name: str,
database: Annotated[str, DATABASE_OPTION] = None,
name_filter: Annotated[str, FILTER_OPTION] = None,
):
"""
List views
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for view in tm1.views.get_all_names(cube_name):
private_views, public_views = tm1.views.get_all_names(cube_name)
for view in apply_filter(private_views + public_views, name_filter):
print(view)


Expand Down
4 changes: 4 additions & 0 deletions tm1cli/utils/cli_param.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import typer

DATABASE_OPTION = typer.Option("--database", "-d", help="Specify the database to use")
FILTER_OPTION = typer.Option(
"--filter",
help="Filter names using a case-insensitive glob pattern.",
)
WATCH_OPTION = typer.Option(
"--watch", "-w", help="Watch the command output periodically."
)
Expand Down
17 changes: 17 additions & 0 deletions tm1cli/utils/list_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Shared utilities for list commands."""

from fnmatch import fnmatchcase
from typing import Optional


def apply_filter(names: list[str], pattern: Optional[str]) -> list[str]:
"""Filter names using a case-insensitive glob pattern."""
if pattern is None:
return names

normalized_pattern = pattern.casefold()
return [
name
for name in names
if fnmatchcase(name.casefold(), normalized_pattern)
]
Loading