From 6ff505c858bf4e6b853a0ae08a6bda7d34fabf99 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Mon, 4 Dec 2023 17:44:18 -0500 Subject: [PATCH 01/10] working add_measure --- pyproject.toml | 2 +- pytabular/measure.py | 75 ++++++++++++++++++++++++++++++++++-- pytabular/object.py | 12 +++++- pytabular/pytabular.py | 2 +- pytabular/table.py | 3 +- pytabular/tabular_tracing.py | 2 +- 6 files changed, 87 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 06bb48e..189b31d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python_tabular" -version = "0.4.0" +version = "0.5.0" authors = [ { name="Curtis Stallings", email="curtisrstallings@gmail.com" }, ] diff --git a/pytabular/measure.py b/pytabular/measure.py index df79a4f..e0f4f69 100644 --- a/pytabular/measure.py +++ b/pytabular/measure.py @@ -6,6 +6,8 @@ import logging import pandas as pd from pytabular.object import PyObject, PyObjects +from Microsoft.AnalysisServices.Tabular import Measure, Table + logger = logging.getLogger("PyTabular") @@ -27,7 +29,6 @@ def __init__(self, object, table) -> None: table (table.PyTable): The parent `PyTable`. """ super().__init__(object) - self.Table = table self._display.add_row("Expression", self._object.Expression, end_section=True) self._display.add_row("DisplayFolder", self._object.DisplayFolder) @@ -59,6 +60,74 @@ class PyMeasures(PyObjects): `model.Measures.find('ratio')`. """ - def __init__(self, objects) -> None: + def __init__(self, objects, parent) -> None: """Extends init from `PyObjects`.""" - super().__init__(objects) + super().__init__(objects, parent) + + def __call__(self, *args, **kwargs): + """Made `PyMeasures` just sends args through to `add_measure`.""" + return self.add_measure(*args, **kwargs) + + def add_measure(self, name: str, expression: str, **kwargs) -> PyMeasure: + """Add or replace measures from `PyMeasures` class. + + Required is just `name` and `expression`. + But you can pass through any properties you wish to update as a kwarg. + This method is also used when calling the class, + so you can create a new measure that way. + kwargs will be set via the `settr` built in function. + Anything in the .Net Measures properties should be viable. + [Measure Class](https://learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.measure?#properties) # noqa: E501 + + Example: + ``` + expr = "SUM('Orders'[Amount])" + model.Measures.add_measure("Orders Total", expr) + ``` + + ``` + expr = "SUM('Orders'[Amount])" + model.Measures.add_measure("Orders Total", expr, Folder = 'Measures') + ``` + + ``` + expr = "SUM('Orders'[Amount])" + model.Tables['Sales'].Measures('Total Sales', expr, Folder = 'Measures') + ``` + + Args: + name (str): Name of the measure. Brackets ARE NOT required. + expression (str): DAX expression for the measure. + """ + if isinstance(self.parent._object, Table): + table = self.parent + model = self.parent.Model + else: + table = self.parent.Tables._first_visible_object() + model = self.parent + + logger.debug(f"Creating measure in {table.Name}") + + new = True + + try: + logger.debug(f"Measure {name} exists... Overwriting...") + new_measure = self.parent.Measures[name]._object + new = False + except IndexError: + logger.debug(f"Creating new measure {name}") + new_measure = Measure() + + new_measure.set_Name(name) + new_measure.set_Expression(expression) + + for key, value in kwargs.items(): + logger.debug(f"Setting '{key}'='{value}' for {new_measure.Name}") + setattr(new_measure, key, value) + + if new: + measures = table.get_Measures() + measures.Add(new_measure) + + model.save_changes() + return model.Measures[new_measure.Name] diff --git a/pytabular/object.py b/pytabular/object.py index 05262a6..cadec87 100644 --- a/pytabular/object.py +++ b/pytabular/object.py @@ -64,16 +64,18 @@ class PyObjects: Still building out the magic methods to give `PyObjects` more flexibility. """ - def __init__(self, objects) -> None: + def __init__(self, objects: list[PyObject], parent=None) -> None: """Initialization of `PyObjects`. Takes the objects in something that is iterable. Then will build a default `rich` table display. Args: - objects (_type_): _description_ + objects(list[PyObject]): .Net objects. + parent: Parent Object. Defaults to `None`. """ self._objects = objects + self.parent = parent self._display = Table(title=str(self.__class__.mro()[0])) for index, obj in enumerate(self._objects): self._display.add_row(str(index), obj.Name) @@ -121,6 +123,12 @@ def __iadd__(self, obj): self.__init__(self._objects) return self + def _first_visible_object(self): + for object in self: + if object.IsHidden is False: + return object + return None + def find(self, object_str: str): """Finds any or all `PyObject` inside of `PyObjects` that match the `object_str`. diff --git a/pytabular/pytabular.py b/pytabular/pytabular.py index 0fc3605..36eed7d 100644 --- a/pytabular/pytabular.py +++ b/pytabular/pytabular.py @@ -141,7 +141,7 @@ def reload_model_info(self) -> bool: [column for table in self.Tables for column in table.Columns] ) self.Measures = PyMeasures( - [measure for table in self.Tables for measure in table.Measures] + [measure for table in self.Tables for measure in table.Measures], self ) self.Cultures = PyCultures( diff --git a/pytabular/table.py b/pytabular/table.py index fee8a2c..e3bc757 100644 --- a/pytabular/table.py +++ b/pytabular/table.py @@ -64,7 +64,8 @@ def __init__(self, object, model) -> None: [ PyMeasure(measure, self) for measure in self._object.Measures.GetEnumerator() - ] + ], + self, ) self._display.add_row("# of Partitions", str(len(self.Partitions))) self._display.add_row("# of Columns", str(len(self.Columns))) diff --git a/pytabular/tabular_tracing.py b/pytabular/tabular_tracing.py index 130f2b9..8b65f5b 100644 --- a/pytabular/tabular_tracing.py +++ b/pytabular/tabular_tracing.py @@ -198,7 +198,7 @@ def _query_dmv_for_event_categories(self): ) for index, row in df.iterrows(): xml_data = xmltodict.parse(row.Data) - if type(xml_data["EVENTCATEGORY"]["EVENTLIST"]["EVENT"]) == list: + if isinstance(xml_data["EVENTCATEGORY"]["EVENTLIST"]["EVENT"], list): events += [ event for event in xml_data["EVENTCATEGORY"]["EVENTLIST"]["EVENT"] ] From b8e91d72a4c1f499a40a1744a9e9d6bec0c3e078 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Mon, 4 Dec 2023 17:56:28 -0600 Subject: [PATCH 02/10] update docstring --- pytabular/object.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytabular/object.py b/pytabular/object.py index cadec87..9c96844 100644 --- a/pytabular/object.py +++ b/pytabular/object.py @@ -124,6 +124,7 @@ def __iadd__(self, obj): return self def _first_visible_object(self): + """Does what the method is called. Get's first `object.IsHidden is False`.""" for object in self: if object.IsHidden is False: return object From f8fcf1dec16f5d9c33e481fb500f04ba23bb67f7 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Mon, 4 Dec 2023 18:02:07 -0600 Subject: [PATCH 03/10] legacy support for annotations --- pytabular/object.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytabular/object.py b/pytabular/object.py index 9c96844..5ead0cd 100644 --- a/pytabular/object.py +++ b/pytabular/object.py @@ -2,6 +2,7 @@ These classes are used with the others (Tables, Columns, Measures, Partitions, etc.). """ +from __future__ import annotations from abc import ABC from rich.console import Console from rich.table import Table From 9eaff2e0ab5a310f4ee96a416d185cdb871010bf Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Mon, 4 Dec 2023 18:11:54 -0600 Subject: [PATCH 04/10] drop 3.6 coverage --- pyproject.toml | 2 +- test/run_versions.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 189b31d..0766ab2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ ] description = "Connect to your tabular model and perform operations programmatically" readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3.10", "Development Status :: 3 - Alpha", diff --git a/test/run_versions.bat b/test/run_versions.bat index 3eb4727..8694c70 100644 --- a/test/run_versions.bat +++ b/test/run_versions.bat @@ -1,2 +1,2 @@ @echo on -pyenv shell 3.6.8 & python3 -m pytest & pyenv shell 3.7.9 & python3 -m pytest & pyenv shell 3.8.9 & python3 -m pytest & pyenv shell 3.9.13 & python3 -m pytest & pyenv shell 3.10.6 & python3 -m pytest & pause & pause \ No newline at end of file +pyenv shell 3.7.9 & python3 -m pytest & pyenv shell 3.8.9 & python3 -m pytest & pyenv shell 3.9.13 & python3 -m pytest & pyenv shell 3.10.6 & python3 -m pytest & pause & pause \ No newline at end of file From 0e481a7fac6fc8d3355dc9269a65cdc90774a454 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Tue, 5 Dec 2023 11:42:19 -0600 Subject: [PATCH 05/10] update flake8 issue --- pytabular/column.py | 2 +- pytabular/table.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pytabular/column.py b/pytabular/column.py index 3fce682..672528b 100644 --- a/pytabular/column.py +++ b/pytabular/column.py @@ -155,6 +155,6 @@ def query_all(self, query_function: str = "COUNTROWS(VALUES(_))") -> pd.DataFram dax_identifier = f"'{table_name}'[{column_name}]" query_str += f"ROW(\"Table\",\"{table_name}\",\ \"Column\",\"{column_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_identifier)}),\n" + {query_function.replace('_',dax_identifier)}), \n" query_str = f"{query_str[:-2]})" return self[0].Table.Model.query(query_str) diff --git a/pytabular/table.py b/pytabular/table.py index e3bc757..768b8e5 100644 --- a/pytabular/table.py +++ b/pytabular/table.py @@ -181,7 +181,7 @@ def query_all(self, query_function: str = "COUNTROWS(_)") -> pd.DataFrame: table_name = table.get_Name() dax_table_identifier = f"'{table_name}'" query_str += f"ROW(\"Table\",\"{table_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_table_identifier)}),\n" + {query_function.replace('_',dax_table_identifier)}), \n" query_str = f"{query_str[:-2]})" return self[0].Model.query(query_str) From 4dfa2a8fd654afd5fbee26d7b6aadb6c00541eac Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Tue, 5 Dec 2023 11:59:38 -0600 Subject: [PATCH 06/10] fix flake8 issues --- pytabular/column.py | 4 ++-- pytabular/table.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytabular/column.py b/pytabular/column.py index 672528b..1b5fd0f 100644 --- a/pytabular/column.py +++ b/pytabular/column.py @@ -153,8 +153,8 @@ def query_all(self, query_function: str = "COUNTROWS(VALUES(_))") -> pd.DataFram table_name = column.Table.get_Name() column_name = column.get_Name() dax_identifier = f"'{table_name}'[{column_name}]" - query_str += f"ROW(\"Table\",\"{table_name}\",\ + query_str += f"""ROW(\"Table\",\"{table_name}\",\ \"Column\",\"{column_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_identifier)}), \n" + {query_function.replace('_',dax_identifier)}),\n""" query_str = f"{query_str[:-2]})" return self[0].Table.Model.query(query_str) diff --git a/pytabular/table.py b/pytabular/table.py index 768b8e5..b5872f9 100644 --- a/pytabular/table.py +++ b/pytabular/table.py @@ -180,8 +180,8 @@ def query_all(self, query_function: str = "COUNTROWS(_)") -> pd.DataFrame: for table in self: table_name = table.get_Name() dax_table_identifier = f"'{table_name}'" - query_str += f"ROW(\"Table\",\"{table_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_table_identifier)}), \n" + query_str += f"""ROW(\"Table\",\"{table_name}\",\"{query_function}\",\ + {query_function.replace('_',dax_table_identifier)}),\n""" query_str = f"{query_str[:-2]})" return self[0].Model.query(query_str) From 38653317a3c4a3a6398de2f9d8fcebad2730528e Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Tue, 5 Dec 2023 12:04:18 -0600 Subject: [PATCH 07/10] noqa: E231 --- pytabular/column.py | 4 ++-- pytabular/table.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytabular/column.py b/pytabular/column.py index 1b5fd0f..9a65610 100644 --- a/pytabular/column.py +++ b/pytabular/column.py @@ -153,8 +153,8 @@ def query_all(self, query_function: str = "COUNTROWS(VALUES(_))") -> pd.DataFram table_name = column.Table.get_Name() column_name = column.get_Name() dax_identifier = f"'{table_name}'[{column_name}]" - query_str += f"""ROW(\"Table\",\"{table_name}\",\ + query_str += f"ROW(\"Table\",\"{table_name}\",\ \"Column\",\"{column_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_identifier)}),\n""" + {query_function.replace('_',dax_identifier)}),\n" # noqa: E231 query_str = f"{query_str[:-2]})" return self[0].Table.Model.query(query_str) diff --git a/pytabular/table.py b/pytabular/table.py index b5872f9..15b83df 100644 --- a/pytabular/table.py +++ b/pytabular/table.py @@ -180,8 +180,8 @@ def query_all(self, query_function: str = "COUNTROWS(_)") -> pd.DataFrame: for table in self: table_name = table.get_Name() dax_table_identifier = f"'{table_name}'" - query_str += f"""ROW(\"Table\",\"{table_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_table_identifier)}),\n""" + query_str += f"ROW(\"Table\",\"{table_name}\",\"{query_function}\",\ + {query_function.replace('_',dax_table_identifier)}),\n" # noqa: E231 query_str = f"{query_str[:-2]})" return self[0].Model.query(query_str) From a27332a8c8d693bd41dbfc8b474519ba869ee4cd Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Tue, 5 Dec 2023 12:09:29 -0600 Subject: [PATCH 08/10] flake8 ignore rules --- pytabular/column.py | 2 +- pytabular/table.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pytabular/column.py b/pytabular/column.py index 9a65610..6d31305 100644 --- a/pytabular/column.py +++ b/pytabular/column.py @@ -155,6 +155,6 @@ def query_all(self, query_function: str = "COUNTROWS(VALUES(_))") -> pd.DataFram dax_identifier = f"'{table_name}'[{column_name}]" query_str += f"ROW(\"Table\",\"{table_name}\",\ \"Column\",\"{column_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_identifier)}),\n" # noqa: E231 + {query_function.replace('_',dax_identifier)}),\n" # noqa: E231, E261 query_str = f"{query_str[:-2]})" return self[0].Table.Model.query(query_str) diff --git a/pytabular/table.py b/pytabular/table.py index 15b83df..05b2804 100644 --- a/pytabular/table.py +++ b/pytabular/table.py @@ -181,7 +181,7 @@ def query_all(self, query_function: str = "COUNTROWS(_)") -> pd.DataFrame: table_name = table.get_Name() dax_table_identifier = f"'{table_name}'" query_str += f"ROW(\"Table\",\"{table_name}\",\"{query_function}\",\ - {query_function.replace('_',dax_table_identifier)}),\n" # noqa: E231 + {query_function.replace('_',dax_table_identifier)}),\n" # noqa: E231, E261 query_str = f"{query_str[:-2]})" return self[0].Model.query(query_str) From c49d9b1c6b9311941e40a50a1ea27640a0d1090d Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Tue, 5 Dec 2023 12:13:15 -0600 Subject: [PATCH 09/10] set specific docstr coverage version --- .github/workflows/docstr-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docstr-coverage.yml b/.github/workflows/docstr-coverage.yml index 18811f1..a64f3cf 100644 --- a/.github/workflows/docstr-coverage.yml +++ b/.github/workflows/docstr-coverage.yml @@ -27,5 +27,5 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v2 - run: pip install --upgrade pip - - run: pip install docstr-coverage + - run: pip install docstr-coverage==2.2.0 - run: docstr-coverage \ No newline at end of file From a0ab09aa5139108eb444739de005a72b4009a711 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Tue, 5 Dec 2023 12:17:05 -0600 Subject: [PATCH 10/10] set python version in docstr coverage --- .github/workflows/docstr-coverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docstr-coverage.yml b/.github/workflows/docstr-coverage.yml index a64f3cf..2a970da 100644 --- a/.github/workflows/docstr-coverage.yml +++ b/.github/workflows/docstr-coverage.yml @@ -26,6 +26,8 @@ jobs: with: fetch-depth: 0 - uses: actions/setup-python@v2 + with: + python-version: '3.10' - run: pip install --upgrade pip - run: pip install docstr-coverage==2.2.0 - run: docstr-coverage \ No newline at end of file