diff --git a/pyproject.toml b/pyproject.toml index 991dd2939..1745e0aea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,149 +1,149 @@ -[project] -name = "ansys-dynamicreporting-core" -version = "0.10.2.dev0" -authors = [ - {name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}, -] - -maintainers = [ - {name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}, - {name = "Ansys ADR Team", email = "adrteam@ansys.com"}, -] -description = "Python interface to Ansys Dynamic Reporting" -readme = "README.rst" -requires-python = ">=3.10" -keywords = ["dynamicreporting", "pydynamicreporting", "pyansys", "ansys"] -license = {text = "MIT"} -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Natural Language :: English", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Software Development :: Libraries :: Python Modules", -] -dependencies = [ - "docker>=7.1.0", - "pypng>=0.20220715.0", - "requests>=2.32", - "urllib3<3.0.0", - "Pillow>=9.3.0", - "python-dateutil>=2.8.0", - "pytz>=2021.3", - "psutil>=6.0.0", - # core ADR dependencies - "django~=4.2", - "djangorestframework~=3.15", - "django-guardian~=2.4", - "tzlocal~=5.0", - "numpy>=1.23.5,<3", - "python-pptx>=0.6.19,<1", - "pandas>=2.0", - "statsmodels>=0.14", - "scipy<=1.15.3", # breaks ADR if not included. Remove when statsmodels is updated - "docutils>=0.21", - "psycopg[binary]>=3.2.3", - "qtpy>=2.4.3" -] - -[tool.setuptools.packages.find] -where = ["src"] -include = ["ansys.dynamicreporting*"] - -[project.urls] -homepage = "https://github.com/ansys/pydynamicreporting" -documentation = "https://dynamicreporting.docs.pyansys.com/" -changelog = "https://github.com/ansys/pydynamicreporting/blob/main/CHANGELOG.rst" -"Bug Tracker" = "https://github.com/ansys/pydynamicreporting/issues" -repository = "https://github.com/ansys/pydynamicreporting" -ci = "https://github.com/ansys/pydynamicreporting/actions" - -[project.optional-dependencies] -test = [ - "pytest>=8.3.3", - "pytest-cov>=6.0.0", - "pyvista==0.45.3", - "vtk==9.4.2", - "ansys-dpf-core==0.13.8", -] -doc = [ - "ansys-sphinx-theme>=1.1.1", - "numpydoc>=1.8.0", - "Sphinx>=8.0.2", - "sphinx-copybutton>=0.5.2", - "sphinx-gallery>=0.18.0", -] -dev = [ - "build", - "packaging", - "twine", - "ipdb", - "ipython", - "pre-commit>=4.0.1", - "black>=25.0.0", - "isort>=6.0.0", -] - -[build-system] -build-backend = "setuptools.build_meta" -requires = [ - "setuptools>=75.8.0", - "setuptools-scm", -] - -[tool.pytest.ini_options] -tmp_path_retention_policy = "failed" -testpaths = ["tests"] -addopts = "--capture=tee-sys --tb=native -p no:warnings -vv" -markers =[ - "integration:Run integration tests", - "smoke:Run the smoke tests", - "unit:Run the unit tests", - "ado_test: subset of tests to be run in the ADO pipeline for ADR", -] -norecursedirs =[ - ".git", - ".idea", -] -filterwarnings = [ - "ignore:.+:DeprecationWarning" -] - -[tool.coverage.run] -omit = ["*/ansys/dynamicreporting/core/adr_utils.py", "*/ansys/dynamicreporting/core/build_info.py"] -branch = true - -[tool.coverage.report] -show_missing = true -ignore_errors = true - -[tool.coverage.html] -show_contexts = true - -[tool.black] -line-length = 100 - -[tool.isort] -profile = "black" -skip_gitignore = true -force_sort_within_sections = true -line_length = 100 -default_section = "THIRDPARTY" -src_paths = ["doc", "src", "tests"] - -[tool.codespell] -ignore-words = "doc/styles/Vocab/ANSYS/accept.txt" -skip = '*.pyc,*.xml,*.gif,*.png,*.jpg,*.js,*.html,doc/source/examples/**/*.ipynb,*.json,*.gz' -quiet-level = 3 - -[tool.bandit] -targets = ["src"] -recursive = true -number = 3 -severity_level = "high" -require_serial = true -exclude_dirs = [ "venv/*","setup.py","test_cleanup.py","tests/*","doc/*" ] \ No newline at end of file +[project] +name = "ansys-dynamicreporting-core" +version = "0.10.2.dev1" +authors = [ + {name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}, +] + +maintainers = [ + {name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}, + {name = "Ansys ADR Team", email = "adrteam@ansys.com"}, +] +description = "Python interface to Ansys Dynamic Reporting" +readme = "README.rst" +requires-python = ">=3.10" +keywords = ["dynamicreporting", "pydynamicreporting", "pyansys", "ansys"] +license = {text = "MIT"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Natural Language :: English", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "docker>=7.1.0", + "pypng>=0.20220715.0", + "requests>=2.32", + "urllib3<3.0.0", + "Pillow>=9.3.0", + "python-dateutil>=2.8.0", + "pytz>=2021.3", + "psutil>=6.0.0", + # core ADR dependencies + "django~=4.2", + "djangorestframework~=3.15", + "django-guardian~=2.4", + "tzlocal~=5.0", + "numpy>=1.23.5,<3", + "python-pptx>=0.6.19,<1", + "pandas>=2.0", + "statsmodels>=0.14", + "scipy<=1.15.3", # breaks ADR if not included. Remove when statsmodels is updated + "docutils>=0.21", + "psycopg[binary]>=3.2.3", + "qtpy>=2.4.3" +] + +[tool.setuptools.packages.find] +where = ["src"] +include = ["ansys.dynamicreporting*"] + +[project.urls] +homepage = "https://github.com/ansys/pydynamicreporting" +documentation = "https://dynamicreporting.docs.pyansys.com/" +changelog = "https://github.com/ansys/pydynamicreporting/blob/main/CHANGELOG.rst" +"Bug Tracker" = "https://github.com/ansys/pydynamicreporting/issues" +repository = "https://github.com/ansys/pydynamicreporting" +ci = "https://github.com/ansys/pydynamicreporting/actions" + +[project.optional-dependencies] +test = [ + "pytest>=8.3.3", + "pytest-cov>=6.0.0", + "pyvista==0.45.3", + "vtk==9.4.2", + "ansys-dpf-core==0.13.8", +] +doc = [ + "ansys-sphinx-theme>=1.1.1", + "numpydoc>=1.8.0", + "Sphinx>=8.0.2", + "sphinx-copybutton>=0.5.2", + "sphinx-gallery>=0.18.0", +] +dev = [ + "build", + "packaging", + "twine", + "ipdb", + "ipython", + "pre-commit>=4.0.1", + "black>=25.0.0", + "isort>=6.0.0", +] + +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=75.8.0", + "setuptools-scm", +] + +[tool.pytest.ini_options] +tmp_path_retention_policy = "failed" +testpaths = ["tests"] +addopts = "--capture=tee-sys --tb=native -p no:warnings -vv" +markers =[ + "integration:Run integration tests", + "smoke:Run the smoke tests", + "unit:Run the unit tests", + "ado_test: subset of tests to be run in the ADO pipeline for ADR", +] +norecursedirs =[ + ".git", + ".idea", +] +filterwarnings = [ + "ignore:.+:DeprecationWarning" +] + +[tool.coverage.run] +omit = ["*/ansys/dynamicreporting/core/adr_utils.py", "*/ansys/dynamicreporting/core/build_info.py"] +branch = true + +[tool.coverage.report] +show_missing = true +ignore_errors = true + +[tool.coverage.html] +show_contexts = true + +[tool.black] +line-length = 100 + +[tool.isort] +profile = "black" +skip_gitignore = true +force_sort_within_sections = true +line_length = 100 +default_section = "THIRDPARTY" +src_paths = ["doc", "src", "tests"] + +[tool.codespell] +ignore-words = "doc/styles/Vocab/ANSYS/accept.txt" +skip = '*.pyc,*.xml,*.gif,*.png,*.jpg,*.js,*.html,doc/source/examples/**/*.ipynb,*.json,*.gz' +quiet-level = 3 + +[tool.bandit] +targets = ["src"] +recursive = true +number = 3 +severity_level = "high" +require_serial = true +exclude_dirs = [ "venv/*","setup.py","test_cleanup.py","tests/*","doc/*" ] diff --git a/src/ansys/dynamicreporting/core/constants.py b/src/ansys/dynamicreporting/core/constants.py index 6d6979d13..d18b38a6c 100644 --- a/src/ansys/dynamicreporting/core/constants.py +++ b/src/ansys/dynamicreporting/core/constants.py @@ -24,6 +24,7 @@ GENERATOR_TYPES = ( "Generator:tablemerge", "Generator:tablereduce", + "Generator:tablemap", "Generator:tablerowcolumnfilter", "Generator:tablevaluefilter", "Generator:tablesortfilter", diff --git a/src/ansys/dynamicreporting/core/serverless/__init__.py b/src/ansys/dynamicreporting/core/serverless/__init__.py index 9249db308..0bb94fcef 100644 --- a/src/ansys/dynamicreporting/core/serverless/__init__.py +++ b/src/ansys/dynamicreporting/core/serverless/__init__.py @@ -24,6 +24,7 @@ SQLQueryGenerator, StatisticalGenerator, TabLayout, + TableMapGenerator, TableMergeGenerator, TableMergeRCFilterGenerator, TableMergeValueFilterGenerator, @@ -70,6 +71,7 @@ "TableReduceGenerator", "TableMergeRCFilterGenerator", "TableMergeValueFilterGenerator", + "TableMapGenerator", "TableSortFilterGenerator", "TreeMergeGenerator", "SQLQueryGenerator", diff --git a/src/ansys/dynamicreporting/core/serverless/template.py b/src/ansys/dynamicreporting/core/serverless/template.py index 53bf97cc0..e3e0f82ad 100644 --- a/src/ansys/dynamicreporting/core/serverless/template.py +++ b/src/ansys/dynamicreporting/core/serverless/template.py @@ -35,6 +35,7 @@ class ReportType(StrEnum): # Generators TABLE_MERGE_GENERATOR = "Generator:tablemerge" TABLE_REDUCE_GENERATOR = "Generator:tablereduce" + TABLE_MAP_GENERATOR = "Generator:tablemap" TABLE_ROW_COLUMN_FILTER_GENERATOR = "Generator:tablerowcolumnfilter" TABLE_VALUE_FILTER_GENERATOR = "Generator:tablevaluefilter" TABLE_SORT_FILTER_GENERATOR = "Generator:tablesortfilter" @@ -623,6 +624,10 @@ class TableReduceGenerator(Generator): report_type: str = ReportType.TABLE_REDUCE_GENERATOR +class TableMapGenerator(Generator): + report_type: str = ReportType.TABLE_MAP_GENERATOR + + class TableMergeRCFilterGenerator(Generator): report_type: str = ReportType.TABLE_ROW_COLUMN_FILTER_GENERATOR diff --git a/src/ansys/dynamicreporting/core/utils/report_objects.py b/src/ansys/dynamicreporting/core/utils/report_objects.py index 86a1958a7..a6fe3d86e 100755 --- a/src/ansys/dynamicreporting/core/utils/report_objects.py +++ b/src/ansys/dynamicreporting/core/utils/report_objects.py @@ -2505,20 +2505,22 @@ def delete_operation(self, name=None): d = json.loads(self.params) if "reduce_params" not in d: return - if "operations" not in d: + if "operations" not in d["reduce_params"]: return sources = d["reduce_params"]["operations"] + index = 0 valid = 0 - for _, s in enumerate(sources): + for i, s in enumerate(sources): compare = [] for iname in shlex.split(s["source_rows"]): compare.append(iname.replace(",", "")) if compare == name: valid = 1 + index = i break if valid == 0: raise ValueError("Error: no existing source with the passed input") - del sources[i] + del sources[index] d["reduce_params"]["operations"] = sources self.params = json.dumps(d) return @@ -2621,6 +2623,163 @@ def set_numeric_output(self, value=0): return +class tablemapREST(GeneratorREST): + """Representation of Table Mathematical Function Mapper Generator Template.""" + + def __init__(self): + super().__init__() + + def get_map_param(self): + d = json.loads(self.params) + if "map_params" in d: + if "map_type" in d["map_params"]: + return d["map_params"]["map_type"] + return "row" + + def set_map_param(self, value="row"): + if not isinstance(value, str): + raise ValueError("Error: input should be a string") + if value not in ("row", "column"): + raise ValueError("Error: input should be either row or column") + d = json.loads(self.params) + if "map_params" not in d: + d["map_params"] = {} + d["map_params"]["map_type"] = value + self.params = json.dumps(d) + return + + def get_table_name(self): + d = json.loads(self.params) + if "map_params" in d: + if "table_name" in d["map_params"]: + return d["map_params"]["table_name"] + return "" + + def set_table_name(self, value="output_table"): + if not isinstance(value, str): + raise ValueError("Error: input should be a string") + d = json.loads(self.params) + if "map_params" not in d: + d["map_params"] = {} + d["map_params"]["table_name"] = value + self.params = json.dumps(d) + return + + def get_operations(self): + d = json.loads(self.params) + if "map_params" in d: + if "operations" in d["map_params"]: + return d["map_params"]["operations"] + return [] + + def delete_operation(self, name=None): + if name is None: + name = [] + if not isinstance(name, list): + raise ValueError( + "Error: need to pass the operation with the source row/column name as a list of strings" + ) + if len([x for x in name if isinstance(x, str)]) != len(name): + raise ValueError("Error: the elements of the input list should all be strings") + d = json.loads(self.params) + if "map_params" not in d: + return + if "operations" not in d["map_params"]: + return + sources = d["map_params"]["operations"] + index = 0 + valid = False + for i, s in enumerate(sources): + compare = [] + for iname in shlex.split(s["source_rows"]): + compare.append(iname.replace(",", "")) + if compare == name: + index = i + valid = True + break + if not valid: + raise ValueError("Error: no existing source with the passed input") + del sources[index] + d["map_params"]["operations"] = sources + self.params = json.dumps(d) + return + + def add_operation( + self, + name=None, + output_name="output row", + select_names="*", + operation="x", + ): + if name is None: + name = ["*"] + d = json.loads(self.params) + if not isinstance(name, list): + raise ValueError("Error: row/column name should be a list of strings") + if len([x for x in name if isinstance(x, str)]) != len(name): + raise ValueError("Error: the elements of the input list should all be strings") + if not isinstance(output_name, str): + raise ValueError("Error: output_name should be a string") + if not isinstance(select_names, str): + raise ValueError("Error: select_names should be a string") + if not isinstance(operation, str): + raise ValueError("Error: operation should be a string") + + if "map_params" not in d: + d["map_params"] = {} + if "operations" not in d["map_params"]: + sources = [] + else: + sources = d["map_params"]["operations"] + new_source = {} + new_source["source_rows"] = ", ".join(repr(x) for x in name) + new_source["output_rows"] = output_name + new_source["output_columns_select"] = select_names + new_source["operation"] = operation + sources.append(new_source) + d["map_params"]["operations"] = sources + self.params = json.dumps(d) + return + + def get_table_transpose(self): + d = json.loads(self.params) + if "map_params" in d: + if "transpose_output" in d["map_params"]: + return d["map_params"]["transpose_output"] + return 0 + + def set_table_transpose(self, value=0): + if not isinstance(value, int): + raise ValueError("Error: the transpose input should be integer") + if value not in (0, 1): + raise ValueError("Error: input value should be 0 or 1") + d = json.loads(self.params) + if "map_params" not in d: + d["map_params"] = {} + d["map_params"]["transpose_output"] = value + self.params = json.dumps(d) + return + + def get_numeric_output(self): + d = json.loads(self.params) + if "map_params" in d: + if "force_numeric" in d["map_params"]: + return d["map_params"]["force_numeric"] + return 0 + + def set_numeric_output(self, value=0): + if not isinstance(value, int): + raise ValueError("Error: the numeric output should be integer") + if value not in (0, 1): + raise ValueError("Error: input value should be 0 or 1") + d = json.loads(self.params) + if "map_params" not in d: + d["map_params"] = {} + d["map_params"]["force_numeric"] = value + self.params = json.dumps(d) + return + + class tablerowcolumnfilterREST(GeneratorREST): """Representation of Table Row/Column Filter Generator Template.""" diff --git a/tests/smoketest.py b/tests/smoketest.py index b7002de19..5ab5b9459 100644 --- a/tests/smoketest.py +++ b/tests/smoketest.py @@ -33,6 +33,7 @@ String, TabLayout, Table, + TableMapGenerator, TableMergeGenerator, TableMergeRCFilterGenerator, TableMergeValueFilterGenerator, diff --git a/tests/test_report_objects.py b/tests/test_report_objects.py index 2d8cc6303..50a899b53 100755 --- a/tests/test_report_objects.py +++ b/tests/test_report_objects.py @@ -1132,13 +1132,12 @@ def test_tablemerge_operation() -> None: a.add_operation(name=["a"]) a.add_operation(name=["b"], existing=False) succ_ten = len(a.get_operations()) == 3 - a.delete_operation() succ_eleven = False try: a.delete_operation(name="a") except ValueError as e: succ_eleven = "need to pass the operation" in str(e) - a.delete_operation(name=["a", "b"]) + a.delete_operation(name=["a"]) succ_a = succ and succ_two and succ_three and succ_four and succ_five and succ_six succ_b = succ_seven and succ_eight and succ_nine and succ_ten and succ_eleven assert succ_a and succ_b @@ -1187,6 +1186,109 @@ def test_tablereduce_numeric() -> None: assert succ and succ_two and succ_three and succ_four +@pytest.mark.ado_test +def test_tablemap_nameparam() -> None: + a = ro.tablemapREST() + assert a.get_map_param() == "row" + try: + a.set_map_param(value=0) + except ValueError as e: + assert "input should be a string" in str(e) + try: + a.set_map_param(value="a") + except ValueError as e: + assert "input should be either row or column" in str(e) + a.set_map_param(value="column") + assert a.get_map_param() == "column" + assert a.get_table_name() == "" + try: + a.set_table_name(value=1) + except ValueError as e: + assert "input should be a string" in str(e) + a.set_table_name(value="abc") + assert a.get_table_name() == "abc" + + +@pytest.mark.ado_test +def test_tablemap_operation() -> None: + a = ro.tablemapREST() + assert a.get_operations() == [] + a.add_operation() + try: + a.add_operation(name="a") + except ValueError as e: + assert "should be a list of strings" in str(e) + try: + a.add_operation(name=[1]) + except ValueError as e: + assert "should all be strings" in str(e) + try: + a.delete_operation(name=[1]) + except ValueError as e: + assert "should all be strings" in str(e) + try: + a.add_operation(output_name=1) + except ValueError as e: + assert "output_name should be a string" in str(e) + try: + a.delete_operation(name=["a"]) + except ValueError as e: + assert "source with the passed input" in str(e) + try: + a.add_operation(select_names=1) + except ValueError as e: + assert "select_names should be a string" in str(e) + try: + a.add_operation(operation=1) + except ValueError as e: + assert "operation should be a string" in str(e) + a.add_operation(name=["a"]) + a.add_operation(name=["b"]) + assert len(a.get_operations()) == 3 + + try: + a.delete_operation(name="a") + except ValueError as e: + assert "need to pass the operation" in str(e) + a.delete_operation(name=["a"]) + + +@pytest.mark.ado_test +def test_tablemap_transpose() -> None: + a = ro.tablemapREST() + assert a.get_table_transpose() == 0 + try: + a.set_table_transpose(value="a") + except ValueError as e: + assert "the transpose input should be integer" in str(e) + try: + a.set_table_transpose(value=3) + except ValueError as e: + assert "input value should be 0 or 1" in str(e) + try: + a.set_table_transpose(value=1.2) + except ValueError as e: + assert "transpose input should be integer" in str(e) + a.set_table_transpose(value=1) + assert a.get_table_transpose() == 1 + + +@pytest.mark.ado_test +def test_tablemap_numeric() -> None: + a = ro.tablemapREST() + assert a.get_numeric_output() == 0 + try: + a.set_numeric_output(value="a") + except ValueError as e: + assert "numeric output should be integer" in str(e) + try: + a.set_numeric_output(value=4) + except ValueError as e: + assert "input value should be 0 or 1" in str(e) + a.set_numeric_output(value=1) + assert a.get_numeric_output() == 1 + + @pytest.mark.ado_test def test_tablerowcol() -> None: a = ro.tablerowcolumnfilterREST()