Skip to content
14 changes: 7 additions & 7 deletions pytabular/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
logger = logging.getLogger("PyTabular")
logger.setLevel(logging.INFO)
logger.info("Logging configured...")
logger.info(f"To update logging:")
logger.info(f">>> import logging")
logger.info(f">>> pytabular.logger.setLevel(level=logging.INFO)")
logger.info(f"See https://docs.python.org/3/library/logging.html#logging-levels")
logger.info("To update logging:")
logger.info(">>> import logging")
logger.info(">>> pytabular.logger.setLevel(level=logging.INFO)")
logger.info("See https://docs.python.org/3/library/logging.html#logging-levels")


logger.info(f"Python Version::{sys.version}")
Expand All @@ -35,7 +35,7 @@
sys.path.append(dll)
sys.path.append(os.path.dirname(__file__))

logger.info(f"Beginning CLR references...")
logger.info("Beginning CLR references...")
import clr

logger.info("Adding Reference Microsoft.AnalysisServices.AdomdClient")
Expand All @@ -45,7 +45,7 @@
logger.info("Adding Reference Microsoft.AnalysisServices")
clr.AddReference("Microsoft.AnalysisServices")

logger.info(f"Importing specifics in module...")
logger.info("Importing specifics in module...")
from .pytabular import Tabular

from .basic_checks import (
Expand All @@ -64,4 +64,4 @@
from .query import Connection
from .pbi_helper import find_local_pbi_instances

logger.info(f"Import successful...")
logger.info("Import successful...")
17 changes: 17 additions & 0 deletions pytabular/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ def __init__(self, object, table) -> None:
self._display.add_row("State", str(self._object.State))
self._display.add_row("DisplayFolder", str(self._object.DisplayFolder))

def get_sample_values(self, top_n: int = 3) -> pd.DataFrame:
"""Get sample values of column."""
column_to_sample = f"'{self.Table.Name}'[{self.Name}]"
dax_query = f"""EVALUATE
TOPNSKIP(
{top_n},
0,
FILTER(
VALUES({column_to_sample}),
NOT ISBLANK({column_to_sample}) && LEN({column_to_sample}) > 0
),
1
)
ORDER BY {column_to_sample}
"""
return self.Table.Model.Query(dax_query)

def Distinct_Count(self, No_Blank=False) -> int:
"""Get [DISTINCTCOUNT](https://learn.microsoft.com/en-us/dax/distinctcount-function-dax) of Column.

Expand Down
49 changes: 49 additions & 0 deletions pytabular/culture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import logging
from object import PyObject, PyObjects

logger = logging.getLogger("PyTabular")


class PyCulture(PyObject):
"""Wrapper for [Microsoft.AnalysisServices.Cultures]

Args:
Table: Parent Table to the Object Translations
"""

def __init__(self, object, model) -> None:
super().__init__(object)
self.Model = model
self._display.add_row("Culture Name", self._object.Name)
self.ObjectTranslations = PyObjectTranslations(
[
PyObjectTranslation(translation, self)
for translation in self._object.ObjectTranslations.GetEnumerator()
]
)


class PyObjectTranslation(PyObject):
"""Wrapper for [Microsoft.AnalysisServices.Cultures]
Args:
Table: Child item of the Culture.
"""

def __init__(self, object, culture) -> None:
self.Name = object.Object.Name
self.ObjectType = object.Object.ObjectType
self.Parent = object.Object.Parent
super().__init__(object)
self.Culture = culture
self._display.add_row("Object Property", str(self._object.Property))
self._display.add_row("Object Value", self._object.Value)


class PyCultures(PyObjects):
def __init__(self, objects) -> None:
super().__init__(objects)


class PyObjectTranslations(PyObjects):
def __init__(self, objects) -> None:
super().__init__(objects)
20 changes: 19 additions & 1 deletion pytabular/measure.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging

import pandas as pd
from object import PyObject, PyObjects

logger = logging.getLogger("PyTabular")
Expand All @@ -15,12 +15,30 @@ class PyMeasure(PyObject):

def __init__(self, object, table) -> None:
super().__init__(object)

self.Table = table
self.Dependancies = self.get_dependancies(object)
self._display.add_row("Expression", self._object.Expression, end_section=True)
self._display.add_row("DisplayFolder", self._object.DisplayFolder)
self._display.add_row("IsHidden", str(self._object.IsHidden))
self._display.add_row("FormatString", self._object.FormatString)

def get_dependencies(self) -> pd.DataFrame:
"""Returns the dependant columns of a measure"""
dmv_query = f"select * from $SYSTEM.DISCOVER_CALC_DEPENDENCY where [OBJECT] = '{self.Name}' and [TABLE] = '{self.Table.Name}'"
return self.Table.Model.Query(dmv_query)

def get_dependancies(self, object) -> pd.DataFrame:
"""Returns the dependant objects of a measure in a dataframe based on the information in $SYSTEM.DISCOVER_CALC_DEPENDENCY"""
return self.Table.Model.Adomd.Query(
f"""
select *
from $SYSTEM.DISCOVER_CALC_DEPENDENCY
where [OBJECT] = '{object.Name}'
and [TABLE] = '{object.Table.Name}'
"""
)


class PyMeasures(PyObjects):
def __init__(self, objects) -> None:
Expand Down
17 changes: 9 additions & 8 deletions pytabular/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ def __init__(self, object) -> None:
"Properties", justify="right", style="cyan", no_wrap=True
)
self._display.add_column("", justify="left", style="magenta", no_wrap=False)
self._display.add_row("Name", self._object.Name)
self._display.add_row("ObjectType", str(self._object.ObjectType))
if not str(self._object.ObjectType) == "Model":
self._display.add_row("ParentName", self._object.Parent.Name)

self._display.add_row("Name", self.Name)
self._display.add_row("ObjectType", str(self.ObjectType))
if str(self.ObjectType) not in "Model":
self._display.add_row("ParentName", self.Parent.Name)
self._display.add_row(
"ParentObjectType",
str(self._object.Parent.ObjectType),
str(self.Parent.ObjectType),
end_section=True,
)

Expand All @@ -28,7 +29,8 @@ def __rich_repr__(self) -> str:
def __getattr__(self, attr):
if attr in self.__dict__:
return getattr(self, attr)
return getattr(self._object, attr)
else:
return getattr(self._object, attr)


class PyObjects:
Expand All @@ -50,8 +52,7 @@ def __getitem__(self, object):
return self._objects[object]

def __iter__(self):
for object in self._objects:
yield object
yield from self._objects

def __len__(self):
return len(self._objects)
Expand Down
40 changes: 21 additions & 19 deletions pytabular/pytabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from partition import PyPartitions
from column import PyColumns
from measure import PyMeasures
from culture import PyCultures, PyCulture
from relationship import PyRelationship, PyRelationships
from object import PyObject
from refresh import PyRefresh
Expand Down Expand Up @@ -76,7 +77,7 @@ def __init__(self, CONNECTION_STR: str):
self.Model = self.Database.Model
logger.info(f"Connected to Model - {self.Model.Name}")
self.Adomd: Connection = Connection(self.Server)
self.Effective_Users = dict()
self.Effective_Users: dict = {}
self.PyRefresh = PyRefresh
# Build PyObjects
self.Reload_Model_Info()
Expand All @@ -87,7 +88,7 @@ def __init__(self, CONNECTION_STR: str):
# Building rich table display for repr
self._display.add_row(
"EstimatedSize",
str(round(self.Database.EstimatedSize / 1000000000, 2)) + " GB",
f"{round(self.Database.EstimatedSize / 1000000000, 2)} GB",
end_section=True,
)
self._display.add_row("# of Tables", str(len(self.Tables)))
Expand All @@ -104,8 +105,6 @@ def __init__(self, CONNECTION_STR: str):
logger.debug("Registering Disconnect on Termination...")
atexit.register(self.Disconnect)

pass

def Reload_Model_Info(self) -> bool:
"""Runs on __init__ iterates through details, can be called after any model changes. Called in SaveChanges()

Expand All @@ -114,6 +113,13 @@ def Reload_Model_Info(self) -> bool:
"""
self.Database.Refresh()

self.Cultures = PyCultures(
[
PyCulture(culture, self)
for culture in self.Model.Cultures.GetEnumerator()
]
)

self.Tables = PyTables(
[PyTable(table, self) for table in self.Model.Tables.GetEnumerator()]
)
Expand Down Expand Up @@ -141,11 +147,7 @@ def Is_Process(self) -> bool:
bool: True if DMV shows Process, False if not.
"""
_jobs_df = self.Query("select * from $SYSTEM.DISCOVER_JOBS")
return (
True
if len(_jobs_df[_jobs_df["JOB_DESCRIPTION"] == "Process"]) > 0
else False
)
return len(_jobs_df[_jobs_df["JOB_DESCRIPTION"] == "Process"]) > 0

def Disconnect(self) -> bool:
"""Disconnects from Model
Expand Down Expand Up @@ -446,16 +448,16 @@ def Query(
"""
if Effective_User is None:
return self.Adomd.Query(Query_Str)
else:
try:
conn = self.Effective_Users[Effective_User]
if isinstance(conn, Connection):
conn.Query(Query_Str)
except Exception:
conn = Connection(self.Server, Effective_User=Effective_User)
self.Effective_Users[Effective_User] = conn

return conn.Query(Query_Str)

try:
conn = self.Effective_Users[Effective_User]
if isinstance(conn, Connection):
conn.Query(Query_Str)
except Exception:
conn = Connection(self.Server, Effective_User=Effective_User)
self.Effective_Users[Effective_User] = conn

return conn.Query(Query_Str)

def Query_Every_Column(
self, query_function: str = "COUNTROWS(VALUES(_))"
Expand Down
47 changes: 47 additions & 0 deletions pytabular/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
def dataframe_to_dict(df):
"""
Convert to Dataframe to dictionary and
alter columns names with;
- Underscores (_) to spaces
- All Strings are converted to Title Case.
"""
list_of_dicts = df.to_dict("records")
return [
{k.replace("_", " ").title(): v for k, v in dict.items()}
for dict in list_of_dicts
]


def dict_to_markdown_table(list_of_dicts: list, columns_to_include: list = None):
"""
Description: Generate a Markdown table based on a list of dictionaries.
Args:
list_of_dicts -> List of Dictionaries that need to be converted
to a markdown table.
columns_to_include -> Default = None, and all colums are included.
If a list is supplied, those columns will be included.
Example:
columns = ['Referenced Object Type', 'Referenced Table', 'Referenced Object']
dict_to_markdown_table(dependancies, columns)

Result:
| Referenced Object Type | Referenced Table | Referenced Object |
| ---------------------- | ---------------- | ------------------------------- |
| TABLE | Cases | Cases |
| COLUMN | Cases | IsClosed |
| CALC_COLUMN | Cases | Resolution Time (Working Hours) |

"""
keys = set().union(*[set(d.keys()) for d in list_of_dicts])

if columns_to_include is not None:
keys = list(keys.intersection(columns_to_include))

table_header = f"| {' | '.join(map(str, keys))} |"
table_header_separator = "|-----" * len(keys) + "|"
markdown_table = [table_header, table_header_separator]

for row in list_of_dicts:
table_row = f"| {' | '.join(str(row.get(key, '')) for key in keys)} |"
markdown_table.append(table_row)
return "\n".join(markdown_table)
Binary file modified test/adventureworks/AdventureWorks Sales.pbix
Binary file not shown.