Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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