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
11 changes: 8 additions & 3 deletions 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 = False):
return self.cubes

def exists(self, cube_name: str):
Expand All @@ -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 All @@ -35,7 +35,7 @@ def exists(self, dimension_name: str):


class MockedSubsetService:
def get_all_names(self, dimension_name: str):
def get_all_names(self, dimension_name: str, hierarchy_name: str = None, private: bool = False):
return ["Subset1", "Subset2", "Subset3"]

def exists(self, dimension_name: str, subset_name: str, private: bool):
Expand All @@ -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, skip_control_processes: bool = False):
return self.processes

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

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cmd_cubes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_cube_list(mocker, command):
result = runner.invoke(app, ["cube", command])
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "Cube1\nCube2\n"
assert result.stdout == "- Cube1\n- Cube2\n"


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cmd_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_dimension_list(mocker, command):
result = runner.invoke(app, ["dimension", command])
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "Dimension1\nDimension2\nDimension3\n"
assert result.stdout == "- Dimension1\n- Dimension2\n- Dimension3\n"


@pytest.mark.parametrize(
Expand Down
49 changes: 48 additions & 1 deletion tests/test_cmd_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@pytest.mark.parametrize(
"raw_option,expected_output",
[(None, " Process exists!\n"), ("--output-raw", "True\n")],
[(None, "\u2705 Process exists!\n"), ("--output-raw", "True\n")],
)
def test_process_exists(mocker, raw_option, expected_output):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
Expand All @@ -20,3 +20,50 @@ def test_process_exists(mocker, raw_option, expected_output):
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == expected_output

@pytest.mark.parametrize("command", ["list", "ls"])
def test_process_list(mocker, command):
mocker.patch("tm1cli.commands.process.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["process", command])
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "- Process1\n- Process2\n"


def test_process_list_json(mocker):
mocker.patch("tm1cli.commands.process.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["process", "list", "--output", "json"])
assert result.exit_code == 0
assert '"Process1"' in result.stdout
assert '"Process2"' in result.stdout


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


def test_process_list_limit(mocker):
mocker.patch("tm1cli.commands.process.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["process", "list", "--limit", "1"])
assert result.exit_code == 0
assert "Process1" in result.stdout
assert "Process2" not in result.stdout


def test_process_list_offset(mocker):
mocker.patch("tm1cli.commands.process.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["process", "list", "--offset", "1"])
assert result.exit_code == 0
assert "Process1" not in result.stdout
assert "Process2" in result.stdout


def test_process_not_exists(mocker):
mocker.patch("tm1cli.utils.generic.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["--output-raw", "process", "exists", "process_notfound"])
assert result.exit_code == 0
assert result.stdout == "False\n"
6 changes: 4 additions & 2 deletions tests/test_cmd_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
@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"])
result = runner.invoke(app, ["subset", command, "--dimension", "Dimension1"])
assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "Subset1\nSubset2\nSubset3\n"
assert "dimension: Dimension1" in result.stdout
assert "name: Subset1" in result.stdout
assert "type: public" in result.stdout


@pytest.mark.parametrize(
Expand Down
6 changes: 4 additions & 2 deletions tests/test_cmd_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ def test_view_exists(mocker, view_name, private_flag, exists_result, raw_option)

def test_view_list(mocker):
mocker.patch("tm1cli.commands.view.TM1Service", MockedTM1Service)
result = runner.invoke(app, ["view", "list", "example_cube"])
result = runner.invoke(app, ["view", "list", "--cube", "example_cube"])

assert result.exit_code == 0
assert isinstance(result.stdout, str)
assert result.stdout == "View1\nView2\nView3\n"
assert "cube: example_cube" in result.stdout
assert "name: View1" in result.stdout
assert "type: public" in result.stdout
10 changes: 8 additions & 2 deletions tests/test_tm1cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from typer.testing import CliRunner

from tests.conftest import MockedTM1Service
from tm1cli.main import app

runner = CliRunner()
Expand Down Expand Up @@ -59,8 +60,13 @@ def test_process_clone_missing_from_to():
)


def test_process_clone_not_exists():
result = runner.invoke(app, ["process", "clone", "example", "--to", "remotedb"])
def test_process_clone_not_exists(mocker):
mocker.patch("tm1cli.commands.process.TM1Service", MockedTM1Service)
mocker.patch(
"tm1cli.commands.process.resolve_database",
side_effect=lambda ctx, db: {"address": "target", "port": 8080} if db == "remotedb" else {"address": "source", "port": 8005},
)
result = runner.invoke(app, ["process", "clone", "process_notfound", "--to", "remotedb"])
assert result.exit_code == 1
assert "Error: Process does not exist in source database!" in result.output

Expand Down
16 changes: 11 additions & 5 deletions tm1cli/commands/cube.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import Annotated
from typing import Annotated, Optional

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.list_utils import OutputFormat, apply_filter, apply_paging, render_output
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -22,17 +23,22 @@ def list_cube(
typer.Option(
"-s",
"--skip-control-cubes",
help="Flag for not printing control cubes.",
help="Exclude control cubes (names starting with '}').",
),
] = False,
filter: Annotated[Optional[str], typer.Option("--filter", "-f", help="Regex/substring filter (case-insensitive).")] = None,
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format: yaml (default) or json.")] = "yaml",
limit: Annotated[Optional[int], typer.Option("--limit", help="Maximum number of results to return.")] = None,
offset: Annotated[int, typer.Option("--offset", help="Number of results to skip (for paging).", min=0)] = 0,
):
"""
List cubes
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for cube in tm1.cubes.get_all_names(skip_control_cubes):
print(cube)
names = tm1.cubes.get_all_names(skip_control_cubes=skip_control_cubes)
names = apply_filter(names, filter)
result = apply_paging(names, limit, offset)
typer.echo(render_output(result, output))


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

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.list_utils import OutputFormat, apply_filter, apply_paging, render_output
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -21,18 +22,23 @@ def list_dimension(
bool,
typer.Option(
"-s",
"--skip-control-cubes",
help="Flag for not printing control cubes.",
"--skip-control-dims",
help="Exclude control dimensions (names starting with '}').",
),
] = False,
filter: Annotated[Optional[str], typer.Option("--filter", "-f", help="Regex/substring filter (case-insensitive).")] = None,
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format: yaml (default) or json.")] = "yaml",
limit: Annotated[Optional[int], typer.Option("--limit", help="Maximum number of results to return.")] = None,
offset: Annotated[int, typer.Option("--offset", help="Number of results to skip (for paging).", min=0)] = 0,
):
"""
List dimensions
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for dim in tm1.dimensions.get_all_names(skip_control_dims):
print(dim)
names = tm1.dimensions.get_all_names(skip_control_dims=skip_control_dims)
names = apply_filter(names, filter)
result = apply_paging(names, limit, offset)
typer.echo(render_output(result, output))


@app.command()
Expand Down
22 changes: 18 additions & 4 deletions tm1cli/commands/process.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from pathlib import Path
from typing import Annotated
from typing import Annotated, Optional

import typer
from rich import print # pylint: disable=redefined-builtin
Expand All @@ -9,6 +9,7 @@

from tm1cli.utils.cli_param import DATABASE_OPTION, INTERVAL_OPTION, WATCH_OPTION
from tm1cli.utils.generic import execute_exists
from tm1cli.utils.list_utils import OutputFormat, apply_filter, apply_paging, render_output
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,14 +30,27 @@ def _get_process(name: str, database_config: dict) -> Process:
def list_process(
ctx: typer.Context,
database: Annotated[str, DATABASE_OPTION] = None,
skip_control_tis: Annotated[
bool,
typer.Option(
"-s",
"--skip-control-tis",
help="Exclude control TI processes (names starting with '}').",
),
] = False,
filter: Annotated[Optional[str], typer.Option("--filter", "-f", help="Regex/substring filter (case-insensitive).")] = None,
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format: yaml (default) or json.")] = "yaml",
limit: Annotated[Optional[int], typer.Option("--limit", help="Maximum number of results to return.")] = None,
offset: Annotated[int, typer.Option("--offset", help="Number of results to skip (for paging).", min=0)] = 0,
):
"""
List processes
"""

with TM1Service(**resolve_database(ctx, database)) as tm1:
for process in tm1.processes.get_all_names():
print(process)
names = tm1.processes.get_all_names(skip_control_processes=skip_control_tis)
names = apply_filter(names, filter)
result = apply_paging(names, limit, offset)
typer.echo(render_output(result, output))


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

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.list_utils import OutputFormat, VisibilityType, apply_filter, apply_paging, render_output
from tm1cli.utils.various import resolve_database
from tm1cli.utils.watch import watch_option

Expand All @@ -16,17 +17,44 @@
@app.command(name="list")
def list_subset(
ctx: typer.Context,
dimension_name: str,
# hierarchy_name: str = None,
dimension: Annotated[
Optional[str],
typer.Option("--dimension", help="Scope to a specific dimension name."),
] = None,
hierarchy: Annotated[
Optional[str],
typer.Option("--hierarchy", help="Hierarchy name (defaults to dimension name)."),
] = None,
type: Annotated[
VisibilityType,
typer.Option("--type", "-t", help="Visibility filter: public, private, or both."),
] = "public",
database: Annotated[str, DATABASE_OPTION] = None,
filter: Annotated[Optional[str], typer.Option("--filter", "-f", help="Regex/substring filter (case-insensitive).")] = None,
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format: yaml (default) or json.")] = "yaml",
limit: Annotated[Optional[int], typer.Option("--limit", help="Maximum number of results to return.")] = None,
offset: Annotated[int, typer.Option("--offset", help="Number of results to skip (for paging).", min=0)] = 0,
):
"""
List subsets
"""

include_public = type in ("public", "both")
include_private = type in ("private", "both")
with TM1Service(**resolve_database(ctx, database)) as tm1:
for subset in tm1.subsets.get_all_names(dimension_name):
print(subset)
dim_names = [dimension] if dimension else tm1.dimensions.get_all_names(skip_control_dims=True)
results: list[dict] = []
for dim in dim_names:
hier = hierarchy or dim
if include_public:
for name in tm1.subsets.get_all_names(dim, hier, private=False):
results.append({"dimension": dim, "hierarchy": hier, "name": name, "type": "public"})
if include_private:
for name in tm1.subsets.get_all_names(dim, hier, private=True):
results.append({"dimension": dim, "hierarchy": hier, "name": name, "type": "private"})
if filter:
matching = set(apply_filter([r["name"] for r in results], filter))
results = [r for r in results if r["name"] in matching]
typer.echo(render_output(apply_paging(results, limit, offset), output))


@app.command()
Expand Down
Loading