diff --git a/.github/workflows/docstr-coverage.yml b/.github/workflows/docstr-coverage.yml index 18811f1..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 + - run: pip install docstr-coverage==2.2.0 - run: docstr-coverage \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 06bb48e..0766ab2 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" }, ] @@ -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/pytabular/column.py b/pytabular/column.py index 3fce682..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" + {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/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..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 @@ -64,16 +65,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 +124,13 @@ def __iadd__(self, obj): self.__init__(self._objects) 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 + 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..05b2804 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))) @@ -180,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" # noqa: E231, E261 query_str = f"{query_str[:-2]})" return self[0].Model.query(query_str) 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"] ] 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