Skip to content

Commit b34121b

Browse files
Curts0Curtis Stallings
andauthored
0.5.0 - Create Measures (#93)
* 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>
1 parent 33ad15d commit b34121b

File tree

9 files changed

+96
-14
lines changed

9 files changed

+96
-14
lines changed

.github/workflows/docstr-coverage.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
with:
2727
fetch-depth: 0
2828
- uses: actions/setup-python@v2
29+
with:
30+
python-version: '3.10'
2931
- run: pip install --upgrade pip
30-
- run: pip install docstr-coverage
32+
- run: pip install docstr-coverage==2.2.0
3133
- run: docstr-coverage

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "python_tabular"
7-
version = "0.4.0"
7+
version = "0.5.0"
88
authors = [
99
{ name="Curtis Stallings", email="curtisrstallings@gmail.com" },
1010
]
@@ -18,7 +18,7 @@ dependencies = [
1818
]
1919
description = "Connect to your tabular model and perform operations programmatically"
2020
readme = "README.md"
21-
requires-python = ">=3.6"
21+
requires-python = ">=3.7"
2222
classifiers = [
2323
"Programming Language :: Python :: 3.10",
2424
"Development Status :: 3 - Alpha",

pytabular/column.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,6 @@ def query_all(self, query_function: str = "COUNTROWS(VALUES(_))") -> pd.DataFram
155155
dax_identifier = f"'{table_name}'[{column_name}]"
156156
query_str += f"ROW(\"Table\",\"{table_name}\",\
157157
\"Column\",\"{column_name}\",\"{query_function}\",\
158-
{query_function.replace('_',dax_identifier)}),\n"
158+
{query_function.replace('_',dax_identifier)}),\n" # noqa: E231, E261
159159
query_str = f"{query_str[:-2]})"
160160
return self[0].Table.Model.query(query_str)

pytabular/measure.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import logging
77
import pandas as pd
88
from pytabular.object import PyObject, PyObjects
9+
from Microsoft.AnalysisServices.Tabular import Measure, Table
10+
911

1012
logger = logging.getLogger("PyTabular")
1113

@@ -27,7 +29,6 @@ def __init__(self, object, table) -> None:
2729
table (table.PyTable): The parent `PyTable`.
2830
"""
2931
super().__init__(object)
30-
3132
self.Table = table
3233
self._display.add_row("Expression", self._object.Expression, end_section=True)
3334
self._display.add_row("DisplayFolder", self._object.DisplayFolder)
@@ -59,6 +60,74 @@ class PyMeasures(PyObjects):
5960
`model.Measures.find('ratio')`.
6061
"""
6162

62-
def __init__(self, objects) -> None:
63+
def __init__(self, objects, parent) -> None:
6364
"""Extends init from `PyObjects`."""
64-
super().__init__(objects)
65+
super().__init__(objects, parent)
66+
67+
def __call__(self, *args, **kwargs):
68+
"""Made `PyMeasures` just sends args through to `add_measure`."""
69+
return self.add_measure(*args, **kwargs)
70+
71+
def add_measure(self, name: str, expression: str, **kwargs) -> PyMeasure:
72+
"""Add or replace measures from `PyMeasures` class.
73+
74+
Required is just `name` and `expression`.
75+
But you can pass through any properties you wish to update as a kwarg.
76+
This method is also used when calling the class,
77+
so you can create a new measure that way.
78+
kwargs will be set via the `settr` built in function.
79+
Anything in the .Net Measures properties should be viable.
80+
[Measure Class](https://learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.measure?#properties) # noqa: E501
81+
82+
Example:
83+
```
84+
expr = "SUM('Orders'[Amount])"
85+
model.Measures.add_measure("Orders Total", expr)
86+
```
87+
88+
```
89+
expr = "SUM('Orders'[Amount])"
90+
model.Measures.add_measure("Orders Total", expr, Folder = 'Measures')
91+
```
92+
93+
```
94+
expr = "SUM('Orders'[Amount])"
95+
model.Tables['Sales'].Measures('Total Sales', expr, Folder = 'Measures')
96+
```
97+
98+
Args:
99+
name (str): Name of the measure. Brackets ARE NOT required.
100+
expression (str): DAX expression for the measure.
101+
"""
102+
if isinstance(self.parent._object, Table):
103+
table = self.parent
104+
model = self.parent.Model
105+
else:
106+
table = self.parent.Tables._first_visible_object()
107+
model = self.parent
108+
109+
logger.debug(f"Creating measure in {table.Name}")
110+
111+
new = True
112+
113+
try:
114+
logger.debug(f"Measure {name} exists... Overwriting...")
115+
new_measure = self.parent.Measures[name]._object
116+
new = False
117+
except IndexError:
118+
logger.debug(f"Creating new measure {name}")
119+
new_measure = Measure()
120+
121+
new_measure.set_Name(name)
122+
new_measure.set_Expression(expression)
123+
124+
for key, value in kwargs.items():
125+
logger.debug(f"Setting '{key}'='{value}' for {new_measure.Name}")
126+
setattr(new_measure, key, value)
127+
128+
if new:
129+
measures = table.get_Measures()
130+
measures.Add(new_measure)
131+
132+
model.save_changes()
133+
return model.Measures[new_measure.Name]

pytabular/object.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
These classes are used with the others (Tables, Columns, Measures, Partitions, etc.).
44
"""
5+
from __future__ import annotations
56
from abc import ABC
67
from rich.console import Console
78
from rich.table import Table
@@ -64,16 +65,18 @@ class PyObjects:
6465
Still building out the magic methods to give `PyObjects` more flexibility.
6566
"""
6667

67-
def __init__(self, objects) -> None:
68+
def __init__(self, objects: list[PyObject], parent=None) -> None:
6869
"""Initialization of `PyObjects`.
6970
7071
Takes the objects in something that is iterable.
7172
Then will build a default `rich` table display.
7273
7374
Args:
74-
objects (_type_): _description_
75+
objects(list[PyObject]): .Net objects.
76+
parent: Parent Object. Defaults to `None`.
7577
"""
7678
self._objects = objects
79+
self.parent = parent
7780
self._display = Table(title=str(self.__class__.mro()[0]))
7881
for index, obj in enumerate(self._objects):
7982
self._display.add_row(str(index), obj.Name)
@@ -121,6 +124,13 @@ def __iadd__(self, obj):
121124
self.__init__(self._objects)
122125
return self
123126

127+
def _first_visible_object(self):
128+
"""Does what the method is called. Get's first `object.IsHidden is False`."""
129+
for object in self:
130+
if object.IsHidden is False:
131+
return object
132+
return None
133+
124134
def find(self, object_str: str):
125135
"""Finds any or all `PyObject` inside of `PyObjects` that match the `object_str`.
126136

pytabular/pytabular.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def reload_model_info(self) -> bool:
141141
[column for table in self.Tables for column in table.Columns]
142142
)
143143
self.Measures = PyMeasures(
144-
[measure for table in self.Tables for measure in table.Measures]
144+
[measure for table in self.Tables for measure in table.Measures], self
145145
)
146146

147147
self.Cultures = PyCultures(

pytabular/table.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def __init__(self, object, model) -> None:
6464
[
6565
PyMeasure(measure, self)
6666
for measure in self._object.Measures.GetEnumerator()
67-
]
67+
],
68+
self,
6869
)
6970
self._display.add_row("# of Partitions", str(len(self.Partitions)))
7071
self._display.add_row("# of Columns", str(len(self.Columns)))
@@ -180,7 +181,7 @@ def query_all(self, query_function: str = "COUNTROWS(_)") -> pd.DataFrame:
180181
table_name = table.get_Name()
181182
dax_table_identifier = f"'{table_name}'"
182183
query_str += f"ROW(\"Table\",\"{table_name}\",\"{query_function}\",\
183-
{query_function.replace('_',dax_table_identifier)}),\n"
184+
{query_function.replace('_',dax_table_identifier)}),\n" # noqa: E231, E261
184185
query_str = f"{query_str[:-2]})"
185186
return self[0].Model.query(query_str)
186187

pytabular/tabular_tracing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _query_dmv_for_event_categories(self):
198198
)
199199
for index, row in df.iterrows():
200200
xml_data = xmltodict.parse(row.Data)
201-
if type(xml_data["EVENTCATEGORY"]["EVENTLIST"]["EVENT"]) == list:
201+
if isinstance(xml_data["EVENTCATEGORY"]["EVENTLIST"]["EVENT"], list):
202202
events += [
203203
event for event in xml_data["EVENTCATEGORY"]["EVENTLIST"]["EVENT"]
204204
]

test/run_versions.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
@echo on
2-
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
2+
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 commit comments

Comments
 (0)