diff --git a/README.md b/README.md index 714e6b8..3b8247b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ model.Tables['Table Name'].Refresh() model.Tables['Table Name'].Partitions['Partition Name'].Refresh() #Default Tracing happens automatically, but can be removed by -- -model.Refresh(['Table1','Table2'], trace = None) +model.Refresh(['Table1','Table2'], Tracing = None) ``` It's not uncommon to need to run through some checks on specific Tables, Partitions, Columns, Etc... diff --git a/mkdocs.yml b/mkdocs.yml index 48dd9f4..1899378 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,7 @@ nav: - Queries: Queries.md - Best Practice Analyzer: Best Practice Analyzer.md - Tabular Editor: Tabular Editor 2.md + - PBI Helper: PBI Helper.md - Logic Utils: Logic Utils.md - Examples: Examples.md theme: readthedocs diff --git a/mkgendocs.yml b/mkgendocs.yml index 8c4036f..b9a6ca5 100644 --- a/mkgendocs.yml +++ b/mkgendocs.yml @@ -51,6 +51,8 @@ pages: - Download_Tabular_Editor classes: - Tabular_Editor + - page: "PBI Helper.md" + - source: 'pytabular/pbi_helper.py' - page: "Examples.md" source: 'pytabular/basic_checks.py' functions: diff --git a/pyproject.toml b/pyproject.toml index e80c6ce..bc384af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python_tabular" -version = "0.2.5" +version = "0.2.6" authors = [ { name="Curtis Stallings", email="curtisrstallings@gmail.com" }, ] diff --git a/pytabular/__init__.py b/pytabular/__init__.py index cb506f2..db09d57 100644 --- a/pytabular/__init__.py +++ b/pytabular/__init__.py @@ -61,5 +61,6 @@ from .tabular_editor import Tabular_Editor from .best_practice_analyzer import BPA from .query import Connection +from .pbi_helper import find_local_pbi_instances logger.debug(f"Import successful...") diff --git a/pytabular/pbi_helper.py b/pytabular/pbi_helper.py new file mode 100644 index 0000000..88fd090 --- /dev/null +++ b/pytabular/pbi_helper.py @@ -0,0 +1,86 @@ +import pytabular as p +import subprocess + + +def get_msmdsrv() -> list: + p.logger.debug("Retrieving msmdsrv.exe(s)") + msmdsrv = subprocess.check_output( + [ + "powershell", + """Get-CimInstance -ClassName Win32_Process -Property * -Filter "Name = 'msmdsrv.exe'" | Select-Object -Property ProcessId -ExpandProperty ProcessId""", + ] + ) + msmdsrv_id = msmdsrv.decode().strip().splitlines() + p.logger.debug(f"ProcessId for msmdsrv.exe {msmdsrv_id}") + return msmdsrv_id + + +def get_port_number(msmdsrv: str) -> str: + port = subprocess.check_output( + [ + "powershell", + f"""Get-NetTCPConnection -State Listen -OwningProcess {msmdsrv} | Select-Object -Property LocalPort -First 1 -ExpandProperty LocalPort""", + ] + ) + port_number = port.decode().strip() + p.logger.debug(f"Listening port - {port_number} for msmdsrv.exe - {msmdsrv}") + return port_number + + +def get_parent_id(msmdsrv: str) -> str: + parent = subprocess.check_output( + [ + "powershell", + f"""Get-CimInstance -ClassName Win32_Process -Property * -Filter "ProcessId = {msmdsrv}" | Select-Object -Property ParentProcessId -ExpandProperty ParentProcessId""", + ] + ) + parent_id = parent.decode().strip() + p.logger.debug(f"ProcessId - {parent_id} for parent of msmdsrv.exe - {msmdsrv}") + return parent_id + + +def get_parent_title(parent_id: str) -> str: + pbi_title_suffixes: list = [ + " \u002D Power BI Desktop", # Dash Punctuation - minus hyphen + " \u2212 Power BI Desktop", # Math Symbol - minus sign + " \u2011 Power BI Desktop", # Dash Punctuation - non-breaking hyphen + " \u2013 Power BI Desktop", # Dash Punctuation - en dash + " \u2014 Power BI Desktop", # Dash Punctuation - em dash + " \u2015 Power BI Desktop", # Dash Punctuation - horizontal bar + ] + title = subprocess.check_output( + ["powershell", f"""(Get-Process -Id {parent_id}).MainWindowTitle"""] + ) + title_name = title.decode().strip() + for suffix in pbi_title_suffixes: + title_name = title_name.replace(suffix, "") + p.logger.debug(f"Title - {title_name} for {parent_id}") + return title_name + + +def create_connection_str(port_number: str) -> str: + connection_str = f"Data Source=localhost:{port_number}" + p.logger.debug(f"Local Connection Str - {connection_str}") + return connection_str + + +def find_local_pbi_instances() -> list: + """The real genius is from this [Dax Studio PowerBIHelper](https://github.com/DaxStudio/DaxStudio/blob/master/src/DaxStudio.UI/Utils/PowerBIHelper.cs). + I just wanted it in python not C#, so reverse engineered what DaxStudio did. + It will run some powershell scripts to pull the appropriate info. + Then will spit out a list with tuples inside. + You can use the connection string to connect to your model with pytabular. + + Returns: + list: Example - `[('PBI File Name1','localhost:{port}'),('PBI File Name2','localhost:{port}')]` + """ + instances = get_msmdsrv() + pbi_instances = [] + for instance in instances: + p.logger.debug(f"Building connection for {instance}") + port = get_port_number(instance) + parent = get_parent_id(instance) + title = get_parent_title(parent) + connect_str = create_connection_str(port) + pbi_instances += [(title, connect_str)] + return pbi_instances diff --git a/pytabular/pytabular.py b/pytabular/pytabular.py index dfc81ad..24bf614 100644 --- a/pytabular/pytabular.py +++ b/pytabular/pytabular.py @@ -63,7 +63,7 @@ def __init__(self, CONNECTION_STR: str): self.Database = [ database for database in self.Server.Databases.GetEnumerator() - if database.Name == self.Catalog + if database.Name == self.Catalog or self.Catalog is None ][0] except Exception: err_msg = f"Unable to find Database... {self.Catalog}" diff --git a/pytabular/query.py b/pytabular/query.py index b6c79d7..7886ee9 100644 --- a/pytabular/query.py +++ b/pytabular/query.py @@ -19,9 +19,13 @@ class Connection(AdomdConnection): def __init__(self, Server, Effective_User=None) -> None: super().__init__() - connection_string = ( - f"{Server.ConnectionString}Password='{Server.ConnectionInfo.Password}'" - ) + if Server.ConnectionInfo.Password is None: + connection_string = Server.ConnectionString + else: + connection_string = ( + f"{Server.ConnectionString}Password='{Server.ConnectionInfo.Password}'" + ) + logger.debug(f"{connection_string}") if Effective_User is not None: connection_string += f";EffectiveUserName={Effective_User}" self.ConnectionString = connection_string diff --git a/test/test_2tabular.py b/test/test_2tabular.py index e4fb8e7..e605473 100644 --- a/test/test_2tabular.py +++ b/test/test_2tabular.py @@ -1,6 +1,7 @@ import local import pytest import pandas as pd +import pytabular as p from test.config import testingtablename, testing_parameters @@ -69,3 +70,15 @@ def test_nonetype_decimal_bug(model): } """ assert len(model.Query(query_str)) == 3 + + +@pytest.mark.parametrize("model", testing_parameters) +def test_Table_Last_Refresh_Times(model): + """Really just testing the the function completes successfully and returns df""" + assert isinstance(p.Table_Last_Refresh_Times(model), pd.DataFrame) is True + + +@pytest.mark.parametrize("model", testing_parameters) +def test_Return_Zero_Row_Tables(model): + """Testing that `Return_Zero_Row_Tables`""" + assert isinstance(p.Return_Zero_Row_Tables(model), list) is True