From 469552547c53578a9493772ba5de7d0069fa21ee Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:03:41 +0100 Subject: [PATCH 1/7] Faeture: Command view exists --- poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 1 + tests/__init__.py | 0 tests/conftest.py | 42 +++++++++++++++++++++++++++++++++++++++++ tests/test_cmd_view.py | 23 ++++++++++++---------- tm1cli/commands/view.py | 18 ++++++++++++++++++ 6 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py diff --git a/poetry.lock b/poetry.lock index 69752ce..b8747c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -531,6 +531,23 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "pytz" version = "2024.2" @@ -791,4 +808,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "02d8dcfcd2f62bd3c553c5f0265ace44448b750b2f3b3e9089ef412b8dc7400c" +content-hash = "4b8690168b6908e88cbe63502587650b8ae119baaa96914ab44afcb9e8912d81" diff --git a/pyproject.toml b/pyproject.toml index 0119a1a..341f41e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ pyyaml = "^6.0.2" isort = "^5.13.2" pytest = "^8.3.3" pylint = "^3.3.1" +pytest-mock = "^3.14.0" [tool.poetry.scripts] tm1cli = "tm1cli.main:app" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a20a662 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +from TM1py import Process + + +class MockedViewService: + + def get_all_names(self, cube_name: str): + return ["View1", "View2", "View3"] + + def exists(self, cube_name: str, view_name: str, private: bool): + if "not" in view_name.lower(): + return False + else: + return True + + +class MockedProcessService: + def exists(self, process_name: str): + return False if "not" in process_name else True + + def get(self, process_name: str): + return Process(process_name) + + def update_or_create(self, process: Process): + return f"Process {process.name} was tested mocked." + + +class MockedTM1Service: + def __init__(self, **kwargs) -> None: + self.views = MockedViewService() + self.processes = MockedProcessService() + + def __enter__(self): + """ + Context manager entry point. + """ + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ + Context manager exit point. Clean up resources if needed. + """ + pass diff --git a/tests/test_cmd_view.py b/tests/test_cmd_view.py index 467d79d..34291e6 100644 --- a/tests/test_cmd_view.py +++ b/tests/test_cmd_view.py @@ -3,18 +3,21 @@ from tm1cli.main import app +from .conftest import MockedTM1Service + runner = CliRunner() -@pytest.mark.parametrize("command", ["list", "ls"]) -def test_cube_list(command): - result = runner.invoke( - app, - [ - "view", - command, - "TM1py_tests_annotations_0f680909_74b1_11ef_b4ba_546ceb97bbfb", - ], - ) +@pytest.mark.parametrize( + "options", + [("not", "", "False"), ("example", "-p", "True"), ("not", "--private", "False")], +) +def test_view_exists(mocker, options): + mocker.patch("tm1cli.commands.view.TM1Service", MockedTM1Service) + if options[1]: + result = runner.invoke(app, ["view", "exists", "example_cube", options[0]]) + else: + result = runner.invoke(app, ["view", "exists", "example_cube", options[:1]]) assert result.exit_code == 0 assert isinstance(result.stdout, str) + assert result.stdout == f"{options[2]}\n" diff --git a/tm1cli/commands/view.py b/tm1cli/commands/view.py index 4a4423f..d854cbf 100644 --- a/tm1cli/commands/view.py +++ b/tm1cli/commands/view.py @@ -24,3 +24,21 @@ def list_view( with TM1Service(**resolve_database(ctx, database)) as tm1: for view in tm1.views.get_all_names(cube_name): print(view) + + +@app.command() +def exists( + ctx: typer.Context, + cube_name: str, + view_name: str, + is_private: Annotated[ + bool, typer.Option("-p", "--private", help="Flag to specify if view is private") + ] = False, + database: Annotated[str, DATABASE_OPTION] = None, +): + """ + Exists view + """ + + with TM1Service(**resolve_database(ctx, database)) as tm1: + print(tm1.views.exists(cube_name, view_name, is_private)) From a140460f9b9bb88ccea43e89b3de49b87e4c8ef6 Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:07:49 +0100 Subject: [PATCH 2/7] Added test for view list --- tests/test_cmd_view.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_cmd_view.py b/tests/test_cmd_view.py index 34291e6..104ded4 100644 --- a/tests/test_cmd_view.py +++ b/tests/test_cmd_view.py @@ -21,3 +21,12 @@ def test_view_exists(mocker, options): assert result.exit_code == 0 assert isinstance(result.stdout, str) assert result.stdout == f"{options[2]}\n" + + +def test_view_list(mocker): + mocker.patch("tm1cli.commands.view.TM1Service", MockedTM1Service) + result = runner.invoke(app, ["view", "list", "example_cube"]) + + assert result.exit_code == 0 + assert isinstance(result.stdout, str) + assert result.stdout == "View1\nView2\nView3\n" From ec8640a1b99e8bead17958abbee06e96263ce362 Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:51:51 +0100 Subject: [PATCH 3/7] Tests: Added mocked tests for cube list --- tests/conftest.py | 6 ++++++ tests/test_cmd_cubes.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a20a662..8985683 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,11 @@ from TM1py import Process +class MockedCubeService: + def get_all_names(self, cube_name: str): + return ["Cube1", "Cube2"] + + class MockedViewService: def get_all_names(self, cube_name: str): @@ -28,6 +33,7 @@ class MockedTM1Service: def __init__(self, **kwargs) -> None: self.views = MockedViewService() self.processes = MockedProcessService() + self.cubes = MockedCubeService() def __enter__(self): """ diff --git a/tests/test_cmd_cubes.py b/tests/test_cmd_cubes.py index 3298fd0..aeba121 100644 --- a/tests/test_cmd_cubes.py +++ b/tests/test_cmd_cubes.py @@ -1,13 +1,16 @@ import pytest from typer.testing import CliRunner +from tests.conftest import MockedTM1Service from tm1cli.main import app runner = CliRunner() @pytest.mark.parametrize("command", ["list", "ls"]) -def test_cube_list(command): +def test_cube_list(mocker, command): + mocker.patch("tm1cli.commands.cube.TM1Service", MockedTM1Service) result = runner.invoke(app, ["cube", command]) assert result.exit_code == 0 assert isinstance(result.stdout, str) + assert result.stdout == "Cube1\nCube2\n" From 13c2f26b73dfab8d6e2c8749ffef813aa98c9ed2 Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:57:32 +0100 Subject: [PATCH 4/7] Feature: Cube exists --- tests/conftest.py | 7 ++++++- tests/test_cmd_cubes.py | 8 ++++++++ tm1cli/commands/cube.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8985683..58d9730 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,13 @@ class MockedCubeService: + cubes = ["Cube1", "Cube2"] + def get_all_names(self, cube_name: str): - return ["Cube1", "Cube2"] + return self.cubes + + def exists(self, cube_name: str): + return cube_name in self.cubes class MockedViewService: diff --git a/tests/test_cmd_cubes.py b/tests/test_cmd_cubes.py index aeba121..ed95515 100644 --- a/tests/test_cmd_cubes.py +++ b/tests/test_cmd_cubes.py @@ -14,3 +14,11 @@ def test_cube_list(mocker, command): assert result.exit_code == 0 assert isinstance(result.stdout, str) assert result.stdout == "Cube1\nCube2\n" + + +def test_cube_exists(mocker): + mocker.patch("tm1cli.commands.cube.TM1Service", MockedTM1Service) + result = runner.invoke(app, ["cube", "exists", "Cube1"]) + assert result.exit_code == 0 + assert isinstance(result.stdout, str) + assert result.stdout == "True\n" diff --git a/tm1cli/commands/cube.py b/tm1cli/commands/cube.py index 15c336b..ba9c2d5 100644 --- a/tm1cli/commands/cube.py +++ b/tm1cli/commands/cube.py @@ -31,3 +31,16 @@ def list_cube( with TM1Service(**resolve_database(ctx, database)) as tm1: for cube in tm1.cubes.get_all_names(skip_control_cubes): print(cube) + + +@app.command() +def exists( + ctx: typer.Context, + cube_name: str, + database: Annotated[str, DATABASE_OPTION] = None, +): + """ + Check if cube exists + """ + with TM1Service(**resolve_database(ctx, database)) as tm1: + print(tm1.cubes.exists(cube_name)) From 7546ea51129fddb7d60bc0280eb0dc8aa3929a23 Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:06:29 +0100 Subject: [PATCH 5/7] Docs: Rephrase view exists description --- tm1cli/commands/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm1cli/commands/view.py b/tm1cli/commands/view.py index d854cbf..dc72fbe 100644 --- a/tm1cli/commands/view.py +++ b/tm1cli/commands/view.py @@ -37,7 +37,7 @@ def exists( database: Annotated[str, DATABASE_OPTION] = None, ): """ - Exists view + Check if view exists """ with TM1Service(**resolve_database(ctx, database)) as tm1: From 8454c50e9de160379111f888c3d1d85e4b8d36eb Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:50:09 +0100 Subject: [PATCH 6/7] Feature: New commands dimension and subset --- CHANGELOG.md | 16 +++++++++++++ README.md | 8 +++++++ tests/conftest.py | 24 +++++++++++++++++++ tests/test_cmd_dimension.py | 24 +++++++++++++++++++ tests/test_cmd_subset.py | 24 +++++++++++++++++++ tm1cli/commands/__init__.py | 4 ++-- tm1cli/commands/dimension.py | 46 ++++++++++++++++++++++++++++++++++++ tm1cli/commands/subset.py | 45 +++++++++++++++++++++++++++++++++++ tm1cli/main.py | 2 ++ 9 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 tests/test_cmd_dimension.py create mode 100644 tests/test_cmd_subset.py create mode 100644 tm1cli/commands/dimension.py create mode 100644 tm1cli/commands/subset.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ed41929..2ee62e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v0.1.5 - 2025-16-01 + +### Features + +- New command: `cube exists` +- New command: `view exists` +- New command: `dimension list` +- New command: `dimension exists` +- New command: `subset list` +- New command: `subset exists` + + +### Chore + +- Improved testing + ## v0.1.4 - 2024-11-29 ### Features diff --git a/README.md b/README.md index bcf033b..99345aa 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,16 @@ tm1cli process dump --folder --format tm1cli process load --folder --format tm1cli cube list +tm1cli cube exists + +tm1cli dimension list +tm1cli dimension exists tm1cli view list +tm1cli view exists + +tm1cli subset list +tm1cli subset exists ``` ### All Available Commands diff --git a/tests/conftest.py b/tests/conftest.py index 58d9730..a347c38 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,28 @@ def exists(self, cube_name: str, view_name: str, private: bool): return True +class MockedDimensionService: + def get_all_names(self, skip_control_dims: bool): + return ["Dimension1", "Dimension2", "Dimension3"] + + def exists(self, dimension_name: str): + if "not" in dimension_name.lower(): + return False + else: + return True + + +class MockedSubsetService: + def get_all_names(self, dimension_name: str): + return ["Subset1", "Subset2", "Subset3"] + + def exists(self, dimension_name: str, subset_name: str, private: bool): + if "not" in subset_name.lower(): + return False + else: + return True + + class MockedProcessService: def exists(self, process_name: str): return False if "not" in process_name else True @@ -39,6 +61,8 @@ def __init__(self, **kwargs) -> None: self.views = MockedViewService() self.processes = MockedProcessService() self.cubes = MockedCubeService() + self.dimensions = MockedDimensionService() + self.subsets = MockedSubsetService() def __enter__(self): """ diff --git a/tests/test_cmd_dimension.py b/tests/test_cmd_dimension.py new file mode 100644 index 0000000..e27e379 --- /dev/null +++ b/tests/test_cmd_dimension.py @@ -0,0 +1,24 @@ +import pytest +from typer.testing import CliRunner + +from tests.conftest import MockedTM1Service +from tm1cli.main import app + +runner = CliRunner() + + +@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]) + assert result.exit_code == 0 + assert isinstance(result.stdout, str) + assert result.stdout == "Dimension1\nDimension2\nDimension3\n" + + +def test_dimension_exists(mocker): + mocker.patch("tm1cli.commands.dimension.TM1Service", MockedTM1Service) + result = runner.invoke(app, ["dimension", "exists", "Dimension1"]) + assert result.exit_code == 0 + assert isinstance(result.stdout, str) + assert result.stdout == "True\n" diff --git a/tests/test_cmd_subset.py b/tests/test_cmd_subset.py new file mode 100644 index 0000000..3d6340d --- /dev/null +++ b/tests/test_cmd_subset.py @@ -0,0 +1,24 @@ +import pytest +from typer.testing import CliRunner + +from tests.conftest import MockedTM1Service +from tm1cli.main import app + +runner = CliRunner() + + +@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"]) + assert result.exit_code == 0 + assert isinstance(result.stdout, str) + assert result.stdout == "Subset1\nSubset2\nSubset3\n" + + +def test_subset_exists(mocker): + mocker.patch("tm1cli.commands.subset.TM1Service", MockedTM1Service) + result = runner.invoke(app, ["subset", "exists", "Dimension1", "Subset1"]) + assert result.exit_code == 0 + assert isinstance(result.stdout, str) + assert result.stdout == "True\n" diff --git a/tm1cli/commands/__init__.py b/tm1cli/commands/__init__.py index 89de21b..3a1dc3a 100644 --- a/tm1cli/commands/__init__.py +++ b/tm1cli/commands/__init__.py @@ -1,3 +1,3 @@ -from . import cube, process, view +from . import cube, dimension, process, subset, view -__all__ = ["process", "cube", "view"] +__all__ = ["process", "cube", "view", "dimension", "subset"] diff --git a/tm1cli/commands/dimension.py b/tm1cli/commands/dimension.py new file mode 100644 index 0000000..203c9ce --- /dev/null +++ b/tm1cli/commands/dimension.py @@ -0,0 +1,46 @@ +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 +from tm1cli.utils.various import resolve_database + +app = typer.Typer() + + +@app.command(name="ls", help="Alias for list") +@app.command(name="list") +def list_dimension( + ctx: typer.Context, + database: Annotated[str, DATABASE_OPTION] = None, + skip_control_dims: Annotated[ + bool, + typer.Option( + "-s", + "--skip-control-cubes", + help="Flag for not printing control cubes.", + ), + ] = False, +): + """ + List dimensions + """ + + with TM1Service(**resolve_database(ctx, database)) as tm1: + for dim in tm1.dimensions.get_all_names(skip_control_dims): + print(dim) + + +@app.command() +def exists( + ctx: typer.Context, + dimension_name: str, + database: Annotated[str, DATABASE_OPTION] = None, +): + """ + Check if dimension exists + """ + with TM1Service(**resolve_database(ctx, database)) as tm1: + print(tm1.dimensions.exists(dimension_name)) diff --git a/tm1cli/commands/subset.py b/tm1cli/commands/subset.py new file mode 100644 index 0000000..32360c3 --- /dev/null +++ b/tm1cli/commands/subset.py @@ -0,0 +1,45 @@ +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 +from tm1cli.utils.various import resolve_database + +app = typer.Typer() + + +@app.command(name="ls", help="Alias for list") +@app.command(name="list") +def list_subset( + ctx: typer.Context, + dimension_name: str, + # hierarchy_name: str = None, + database: Annotated[str, DATABASE_OPTION] = None, +): + """ + List subsets + """ + + with TM1Service(**resolve_database(ctx, database)) as tm1: + for subset in tm1.subsets.get_all_names(dimension_name): + print(subset) + + +@app.command() +def exists( + ctx: typer.Context, + dimension_name: str, + subset_name: str, + is_private: Annotated[ + bool, typer.Option("-p", "--private", help="Flag to specify if view is private") + ] = False, + database: Annotated[str, DATABASE_OPTION] = None, +): + """ + Check if subset exists + """ + + with TM1Service(**resolve_database(ctx, database)) as tm1: + print(tm1.views.exists(dimension_name, subset_name, is_private)) diff --git a/tm1cli/main.py b/tm1cli/main.py index 130b3ee..6c828e6 100644 --- a/tm1cli/main.py +++ b/tm1cli/main.py @@ -19,6 +19,8 @@ ("process", commands.process), ("cube", commands.cube), ("view", commands.view), + ("dimension", commands.dimension), + ("subset", commands.subset), ] for name, module in modules: app.add_typer(module.app, name=name) From a1e8db81b33291548d29ba35b6b104a9697c80a8 Mon Sep 17 00:00:00 2001 From: onefloid <33193059+onefloid@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:50:42 +0100 Subject: [PATCH 7/7] Chore: Prepare release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 341f41e..4728bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tm1cli" -version = "0.1.4" +version = "0.1.5" description = "A command-line interface (CLI) tool for interacting with TM1 servers using TM1py." authors = ["onefloid "] license = "MIT License"