Skip to content

Commit

Permalink
0.5.0 - Create Measures (#93)
Browse files Browse the repository at this point in the history
* working add_measure

* update docstring

* legacy support for annotations

* drop 3.6 coverage

* update flake8 issue

* fix flake8 issues

* noqa: E231

* flake8 ignore rules

* set specific docstr coverage version

* set python version in docstr coverage

---------

Co-authored-by: Curtis Stallings <curtis.stallings@rockwellautomation.com>
  • Loading branch information
Curts0 and Curtis Stallings authored Dec 5, 2023
1 parent 33ad15d commit b34121b
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 14 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/docstr-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion pytabular/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
75 changes: 72 additions & 3 deletions pytabular/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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)
Expand Down Expand Up @@ -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]
14 changes: 12 additions & 2 deletions pytabular/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion pytabular/pytabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions pytabular/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pytabular/tabular_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
]
Expand Down
2 changes: 1 addition & 1 deletion test/run_versions.bat
Original file line number Diff line number Diff line change
@@ -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
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

0 comments on commit b34121b

Please sign in to comment.