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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![readthedocs](https://github.com/Curts0/PyTabular/actions/workflows/readthedocs.yml/badge.svg)](https://github.com/Curts0/PyTabular/actions/workflows/readthedocs.yml)
[![pages-build-deployment](https://github.com/Curts0/PyTabular/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/Curts0/PyTabular/actions/workflows/pages/pages-build-deployment)
[![flake8](https://github.com/Curts0/PyTabular/actions/workflows/flake8.yml/badge.svg?branch=master)](https://github.com/Curts0/PyTabular/actions/workflows/flake8.yml)
[![docstr-coverage](https://github.com/Curts0/PyTabular/actions/workflows/docstr-coverage.yml/badge.svg)](https://github.com/Curts0/PyTabular/actions/workflows/docstr-coverage.yml)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
### What is it?

Expand Down
13 changes: 11 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.3.1"
version = "0.3.2"
authors = [
{ name="Curtis Stallings", email="curtisrstallings@gmail.com" },
]
Expand Down Expand Up @@ -40,4 +40,13 @@ include-package-data = true

[tool.pytest.ini_options]
filterwarnings = ["ignore::DeprecationWarning"]
addopts = "-vv"
addopts = "-vv"

[tool.coverage.report]
exclude_lines = [
"pragma: no cover"
]
[tool.coverage.run]
include = [
"pytabular/*"
]
9 changes: 9 additions & 0 deletions pytabular/basic_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ def BPA_Violations_To_DF(model: pytabular.Tabular, te2: str, bpa: str) -> pd.Dat
Returns:
pd.DataFrame: Super simple right now. Just splits into two columns.. The object in violation and the rule.
"""
logger.warning(f"{sys._getframe(0).f_code.co_name} will be deprecated...")
logger.warning(
"Honestly it was dumb I ever made this. You can use model.Analyze_BPA()"
)
results = model.Analyze_BPA(te2, bpa)
data = [
rule.replace(" violates rule ", "^").replace('"', "").split("^")
Expand Down Expand Up @@ -116,6 +120,11 @@ def Last_X_Interval(
Returns:
pd.DataFrame: Pandas DataFrame of results.
"""
# pragma: no cover
logger.warning(f"{sys._getframe(0).f_code.co_name} will be deprecated...")
logger.warning(
"Honestly it was dumb I ever made this. Just write the darn thing in DAX"
)
if isinstance(PyMeasure, str):
try:
Measure = [
Expand Down
4 changes: 3 additions & 1 deletion pytabular/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def get_sample_values(self, top_n: int = 3) -> pd.DataFrame:
ORDER BY {column_to_sample}
"""
return self.Table.Model.Query(dax_query)
except Exception:
except Exception: # pragma: no cover
# Excluding exception from coverage.py for now...
# This is really tech debt anyways and should be replaced...
dax_query = f"""
EVALUATE
TOPN(
Expand Down
58 changes: 6 additions & 52 deletions pytabular/logic_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,43 +52,6 @@ def pandas_datatype_to_tabular_datatype(df: pd.DataFrame) -> Dict:
}


def pd_dataframe_to_dax_expression(
df: pd.DataFrame = pd.DataFrame(data={"col1": [1.0, 2.0], "col2": [3, 4]})
) -> str:
"""
This will take a pandas dataframe and convert to a dax expression
For example this DF:
col1 col2
0 1 3
1 2 4

|
|
V

Will convert to this expression string:
DEFINE
TABLE tablename = { ( 1, 3 ), ( 2, 4 ) }

EVALUATE
SELECTCOLUMNS(
tablename,
"col1", tablename[Value1],
"col2", tablename[Value2]
)
"""

def dax_tableconstructor_rows_expression_generator(
list_of_strings: list[str],
) -> str:
"""
Converts list[str] to dax table rows for example ['one','two'] -> '('one','two')'
"""
return

return True


def pd_dataframe_to_m_expression(df: pd.DataFrame) -> str:
"""This will take a pandas dataframe and convert to an m expression
For example this DF:
Expand Down Expand Up @@ -174,21 +137,12 @@ def remove_suffix(input_string, suffix):
Returns:
str: input_str with suffix removed
"""
if suffix and input_string.endswith(suffix):
return input_string[: -len(suffix)]
return input_string


def sql_wrap_count_around_query(original_query: str) -> str:
"""Simple string formating to get the total row count of a sql query.

Args:
original_query (str): Regular sql query to get count of.

Returns:
str: f"SELECT COUNT(1) FROM ({original_query}) temp_table"
"""
return f"SELECT COUNT(1) FROM ({original_query}) temp_table"
output = (
input_string[: -len(suffix)]
if suffix and input_string.endswith(suffix)
else input_string
)
return output


def get_sub_list(lst: list, n: int) -> list:
Expand Down
38 changes: 15 additions & 23 deletions pytabular/pytabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
MPartitionSource,
)

from Microsoft.AnalysisServices import UpdateOptions
from typing import List, Union
from collections import namedtuple
import pandas as pd
Expand Down Expand Up @@ -71,7 +70,7 @@ def __init__(self, CONNECTION_STR: str):
for database in self.Server.Databases.GetEnumerator()
if database.Name == self.Catalog or self.Catalog is None
][0]
except Exception:
except Exception: # pragma: no cover
err_msg = f"Unable to find Database... {self.Catalog}"
logger.error(err_msg)
raise Exception(err_msg)
Expand Down Expand Up @@ -153,15 +152,17 @@ def Is_Process(self) -> bool:
_jobs_df = self.Query("select * from $SYSTEM.DISCOVER_JOBS")
return len(_jobs_df[_jobs_df["JOB_DESCRIPTION"] == "Process"]) > 0

def Disconnect(self) -> bool:
"""Disconnects from Model

Returns:
bool: True if successful
"""
def Disconnect(self) -> None:
"""Disconnects from Model"""
logger.info(f"Disconnecting from - {self.Server.Name}")
atexit.unregister(self.Disconnect)
return self.Server.Disconnect()

def Reconnect(self) -> None:
"""Reconnects to Model"""
logger.info(f"Reconnecting to {self.Server.Name}")
return self.Server.Reconnect()

def Refresh(self, *args, **kwargs) -> pd.DataFrame:
"""PyRefresh Class to handle refreshes of model.

Expand All @@ -178,22 +179,12 @@ def Refresh(self, *args, **kwargs) -> pd.DataFrame:
"""
return self.PyRefresh(self, *args, **kwargs).Run()

def Update(self, UpdateOptions: UpdateOptions = UpdateOptions.ExpandFull) -> None:
"""[Update Model](https://docs.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.majorobject.update?view=analysisservices-dotnet#microsoft-analysisservices-majorobject-update(microsoft-analysisservices-updateoptions))

Args:
UpdateOptions (UpdateOptions, optional): See above MS Doc link. Defaults to UpdateOptions.ExpandFull.

Returns:
None: Placeholder to eventually change.
"""
logger.debug("Running Update Request")
return self.Database.Update(UpdateOptions)

def SaveChanges(self):
"""Called after refreshes or any model changes.
Currently will return a named tuple of all changes detected. However a ton of room for improvement here.
"""
if self.Server.Connected is False:
self.Reconnect()

def property_changes(Property_Changes):
"""
Expand Down Expand Up @@ -248,7 +239,7 @@ def property_changes(Property_Changes):
Xmla_Results,
)

def Backup_Table(self, table_str: str) -> bool:
def Backup_Table(self, table_str: str) -> bool: # pragma: no cover
"""Will be removed. This is experimental with no written pytest for it.
Backs up table in memory, brings with it measures, columns, hierarchies, relationships, roles, etc.
It will add suffix '_backup' to all objects.
Expand Down Expand Up @@ -350,7 +341,7 @@ def clone_role_permissions():
self.SaveChanges()
return True

def Revert_Table(self, table_str: str) -> bool:
def Revert_Table(self, table_str: str) -> bool: # pragma: no cover
"""Will be removed. This is experimental with no written pytest for it. This is used in conjunction with Backup_Table().
It will take the 'TableName_backup' and replace with the original.
Example scenario ->
Expand Down Expand Up @@ -469,7 +460,8 @@ def Query(
if Effective_User is None:
return self.Adomd.Query(Query_Str)

try:
try: # pragma: no cover
# This needs a public model with effective users to properly test
conn = self.Effective_Users[Effective_User]
logger.debug(f"Effective user found querying as... {Effective_User}")
except Exception:
Expand Down
2 changes: 0 additions & 2 deletions pytabular/refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from table import PyTable, PyTables
from partition import PyPartition
from abc import ABC
import atexit

logger = logging.getLogger("PyTabular")

Expand Down Expand Up @@ -263,7 +262,6 @@ def _post_checks(self):
if self.trace is not None:
self.trace.Stop()
self.trace.Drop()
atexit.unregister(self.trace.Drop)
for check in self._checks:
check.Post_Check()
self._checks.remove_refresh_check(check)
Expand Down
5 changes: 2 additions & 3 deletions pytabular/relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
Once connected to your model, interacting with relationship(s) will be done through these classes.
"""
import logging
from object import PyObject
from pytabular.object import PyObjects
from pytabular.table import PyTable, PyTables
from object import PyObject, PyObjects
from table import PyTable, PyTables

from Microsoft.AnalysisServices.Tabular import (
CrossFilteringBehavior,
Expand Down
18 changes: 7 additions & 11 deletions pytabular/tabular_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def add_column(trace_event, trace_event_column):
"""Adds the column to trace event."""
try:
trace_event.Columns.Add(trace_event_column)
except Exception:
except Exception: # pragma: no cover
logger.warning(f"{trace_event} - {trace_event_column} Skipped")
pass

Expand All @@ -98,14 +98,6 @@ def add_column(trace_event, trace_event_column):
self.Trace.OnEvent += self.Handler
return True

def Arguments(
Trace_Events: List[TraceEvent],
Trace_Event_Columns: List[TraceColumn],
Handler: Callable,
):
"""Raises NotImplementedError. Arguments must be created in order for Trace to work."""
raise NotImplementedError

def Add(self) -> int:
"""Runs on initialization. Adds built Trace to the Server.

Expand All @@ -123,6 +115,9 @@ def Update(self) -> None:
Unless unsuccessful then it will return the error from Server.
"""
logger.info(f"Updating {self.Name} in {self.Tabular_Class.Server.Name}")
if self.Tabular_Class.Server.Connected is False:
self.Tabular_Class.Reconnect()

return self.Trace.Update()

def Start(self) -> None:
Expand Down Expand Up @@ -153,6 +148,7 @@ def Drop(self) -> None:
then it will return the error from Server.
"""
logger.info(f"Dropping {self.Name} in {self.Tabular_Class.Server.Name}")
atexit.unregister(self.Drop)
return self.Trace.Drop()

def _Query_DMV_For_Event_Categories(self):
Expand Down Expand Up @@ -189,7 +185,7 @@ def _Query_DMV_For_Event_Categories(self):
return Event_Categories


def _refresh_handler(source, args):
def _refresh_handler(source, args): # pragma: no cover
"""Default function called when `Refresh_Trace` is used.
It will log various steps of the refresh process.
"""
Expand Down Expand Up @@ -289,7 +285,7 @@ def __init__(
super().__init__(Tabular_Class, Trace_Events, Trace_Event_Columns, Handler)


def _query_monitor_handler(source, args):
def _query_monitor_handler(source, args): # pragma: no cover
"""
Default function used with the `Query_Monitor` trace.
"""
Expand Down
Binary file modified test/adventureworks/AdventureWorks Sales.pbix
Binary file not shown.
26 changes: 15 additions & 11 deletions test/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,29 @@ def get_test_path():

adventureworks_path = f'"{get_test_path()}\\adventureworks\\AdventureWorks Sales.pbix"'

p.logger.info(f"Opening {adventureworks_path}")
subprocess.run(["powershell", f"Start-Process {adventureworks_path}"])

# Got to be a better way to wait and ensure the PBIX file is open?
p.logger.info("sleep(30)... Need a better way to wait until PBIX is loaded...")
sleep(30)
def find_local_pbi():
attempt = p.find_local_pbi_instances()
if len(attempt) > 0:
return attempt[0]
else:
p.logger.info(f"Opening {adventureworks_path}")
subprocess.run(["powershell", f"Start-Process {adventureworks_path}"])
# Got to be a better way to wait and ensure the PBIX file is open?
p.logger.info("sleep(30)... Need a better way to wait until PBIX is loaded...")
sleep(30)
return p.find_local_pbi_instances()[0]


LOCAL_FILE = find_local_pbi()

LOCAL_FILE = p.find_local_pbi_instances()[0]
p.logger.info(f"Connecting to... {LOCAL_FILE[0]} - {LOCAL_FILE[1]}")
local_pbix = p.Tabular(LOCAL_FILE[1])

# subprocess.Popen(["powershell","Start-Process \"AdventureWorks Sales.pbix\""])

p.logger.info("Generating test data...")
testingtablename = "PyTestTable"
testingtabledf = pd.DataFrame(data={"col1": [1, 2, 3], "col2": ["four", "five", "six"]})
testing_parameters = [
pytest.param(local_pbix, id="LOCAL"),
pytest.param(local_pbix, id=local_pbix.Name),
]


# os.path.basename(os.getcwd())
7 changes: 5 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
testingtabledf,
)
import pytabular as p
import subprocess


class testing_storage:
query_trace = None


def pytest_report_header(config):
Expand Down Expand Up @@ -34,5 +37,5 @@ def pytest_sessionfinish(session, exitstatus):
p.logger.info("Executing pytest cleanup...")
remove_testing_table(local_pbix)
p.logger.info("Finding and closing PBIX file...")
subprocess.run(["powershell", "Stop-Process -Name PBIDesktop"])
# subprocess.run(["powershell", "Stop-Process -Name PBIDesktop"])
return True
Loading