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
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[flake8]
ignore = E501, W503
ignore = E501, W503, E203
2 changes: 1 addition & 1 deletion 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.2.4"
version = "0.2.5"
authors = [
{ name="Curtis Stallings", email="curtisrstallings@gmail.com" },
]
Expand Down
21 changes: 20 additions & 1 deletion pytabular/logic_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Dict, List
import pandas as pd
from Microsoft.AnalysisServices.Tabular import DataType
from Microsoft.AnalysisServices.AdomdClient import AdomdDataReader

logger = logging.getLogger("PyTabular")

Expand Down Expand Up @@ -188,4 +189,22 @@ def get_sub_list(lst: list, n: int) -> list:
Returns:
list: Nested list.
"""
return [lst[i: i + n] for i in range(0, len(lst), n)]
return [lst[i : i + n] for i in range(0, len(lst), n)]


def get_value_to_df(Query: AdomdDataReader, index: int):
"""Gets the values from the AdomdDataReader to convert the .Net Object
into a tangible python value to work with in pandas.
Lots of room for improvement on this one.

Args:
Query (AdomdDataReader): [AdomdDataReader](https://learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.adomdclient.adomddatareader?view=analysisservices-dotnet)
index (int): Index of the value to perform the logic on.
"""
if (
Query.GetDataTypeName((index)) in ("Decimal")
and Query.GetValue(index) is not None
):
return Query.GetValue(index).ToDouble(Query.GetValue(index))
else:
return Query.GetValue(index)
10 changes: 10 additions & 0 deletions pytabular/object.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC
from rich.console import Console
from rich.table import Table
from collections.abc import Iterable


class PyObject(ABC):
Expand Down Expand Up @@ -55,6 +56,15 @@ def __iter__(self):
def __len__(self):
return len(self._objects)

def __iadd__(self, obj):
if isinstance(obj, Iterable):
self._objects.__iadd__(obj._objects)
else:
self._objects.__iadd__([obj])

self.__init__(self._objects)
return self

def Find(self, object_str):
items = [
object
Expand Down
3 changes: 2 additions & 1 deletion pytabular/partition.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from object import PyObject, PyObjects
from logic_utils import ticks_to_datetime
import pandas as pd
from datetime import datetime

logger = logging.getLogger("PyTabular")

Expand All @@ -27,7 +28,7 @@ def __init__(self, object, table) -> None:
"RefreshedTime", self.Last_Refresh().strftime("%m/%d/%Y, %H:%M:%S")
)

def Last_Refresh(self):
def Last_Refresh(self) -> datetime:
"""Queries `RefreshedTime` attribute in the partition and converts from C# Ticks to Python datetime

Returns:
Expand Down
2 changes: 1 addition & 1 deletion pytabular/pytabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def Disconnect(self) -> bool:
Returns:
bool: True if successful
"""
logger.debug(f"Disconnecting from - {self.Server.Name}")
logger.info(f"Disconnecting from - {self.Server.Name}")
return self.Server.Disconnect()

def Refresh(self, *args, **kwargs) -> pd.DataFrame:
Expand Down
7 changes: 3 additions & 4 deletions pytabular/query.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from typing import Union
from logic_utils import get_value_to_df
import pandas as pd
from Microsoft.AnalysisServices.AdomdClient import AdomdCommand, AdomdConnection

Expand Down Expand Up @@ -54,18 +55,16 @@ def Query(self, Query_Str: str) -> Union[pd.DataFrame, str, int]:
logger.info(f"Connected! Session ID - {self.SessionID}")

logger.debug("Querying Model...")
logger.debug(Query_Str)
Query = AdomdCommand(Query_Str, self).ExecuteReader()
Column_Headers = [
(index, Query.GetName(index)) for index in range(0, Query.FieldCount)
]
Results = list()
while Query.Read():
"""This is a bit garbage will need to refactor later but fixing issue with System.Decimal conversion"""
Results.append(
[
Query.GetValue(index).ToDouble(Query.GetValue(index))
if Query.GetDataTypeName((index)) in ("Decimal")
else Query.GetValue(index)
get_value_to_df(Query, index)
for index in range(0, len(Column_Headers))
]
)
Expand Down
12 changes: 12 additions & 0 deletions pytabular/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from measure import PyMeasure, PyMeasures
from pytabular.object import PyObjects
from logic_utils import ticks_to_datetime
from datetime import datetime

logger = logging.getLogger("PyTabular")

Expand Down Expand Up @@ -71,6 +72,17 @@ def Refresh(self, *args, **kwargs) -> pd.DataFrame:
"""
return self.Model.Refresh(self, *args, **kwargs)

def Last_Refresh(self) -> datetime:
"""Will query each partition for the last refresh time then select the max

Returns:
datetime: Last refresh time in datetime format
"""
partition_refreshes = [
partition.Last_Refresh() for partition in self.Partitions
]
return max(partition_refreshes)

def Related(self):
return self.Model.Relationships.Related(self)

Expand Down
12 changes: 12 additions & 0 deletions test/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytabular as p
import local
import pandas as pd
import pytest

PLACEHOLDER_AAS = local.AAS
PLACEHOLDER_GEN2 = local.GEN2
aas = p.Tabular(PLACEHOLDER_AAS)
gen2 = p.Tabular(PLACEHOLDER_GEN2)
testingtablename = "PyTestTable"
testingtabledf = pd.DataFrame(data={"col1": [1, 2, 3], "col2": ["four", "five", "six"]})
testing_parameters = [pytest.param(aas, id="AAS"), pytest.param(gen2, id="GEN2")]
33 changes: 33 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from test.config import aas, gen2, testingtablename, testingtabledf
import pytabular as p


def pytest_report_header(config):
return "PyTabular Local Testing"


def remove_testing_table(model):
table_check = [
table
for table in model.Model.Tables.GetEnumerator()
if testingtablename in table.Name
]
for table in table_check:
model.Model.Tables.Remove(table)
model.SaveChanges()


def pytest_sessionstart(session):
p.logger.info("Executing pytest setup...")
remove_testing_table(aas)
remove_testing_table(gen2)
aas.Create_Table(testingtabledf, testingtablename)
gen2.Create_Table(testingtabledf, testingtablename)
return True


def pytest_sessionfinish(session, exitstatus):
p.logger.info("Executing pytest cleanup...")
remove_testing_table(aas)
remove_testing_table(gen2)
return True
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 test_tabular.py & pyenv shell 3.7.9 & python3 -m pytest test_tabular.py & pyenv shell 3.8.9 & python3 -m pytest test_tabular.py & pyenv shell 3.9.13 & python3 -m pytest test_tabular.py & pyenv shell 3.10.6 & python3 -m pytest test_tabular.py & pause & pause
pyenv shell 3.6.8 & python3 -m pytest test_2tabular.py & pyenv shell 3.7.9 & python3 -m pytest test_2tabular.py & pyenv shell 3.8.9 & python3 -m pytest test_2tabular.py & pyenv shell 3.9.13 & python3 -m pytest test_2tabular.py & pyenv shell 3.10.6 & python3 -m pytest test_2tabular.py & pause & pause
25 changes: 25 additions & 0 deletions test/test_1sanity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from test.config import aas, gen2
import pytest
from Microsoft.AnalysisServices.Tabular import Database

testing_parameters = [pytest.param(aas, id="AAS"), pytest.param(gen2, id="GEN2")]


@pytest.mark.parametrize("model", testing_parameters)
def test_sanity_check(model):
"""Just in case... I might be crazy"""
assert 1 == 1


@pytest.mark.parametrize("model", testing_parameters)
def test_connection(model):
"""
Does a quick check to the Tabular Class
To ensure that it can connnect
"""
assert model.Server.Connected


@pytest.mark.parametrize("model", testing_parameters)
def test_database(model):
assert isinstance(model.Database, Database)
71 changes: 71 additions & 0 deletions test/test_2tabular.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import local
import pytest
import pandas as pd
from test.config import testingtablename, testing_parameters


@pytest.mark.parametrize("model", testing_parameters)
def test_basic_query(model):
int_result = model.Query("EVALUATE {1}")
text_result = model.Query('EVALUATE {"Hello World"}')
assert int_result == 1 and text_result == "Hello World"


datatype_queries = [
["this is a string", '"this is a string"'],
[1, 1],
[1000.78, "CONVERT(1000.78,CURRENCY)"],
]


@pytest.mark.parametrize("model", testing_parameters)
def test_datatype_query(model):
for query in datatype_queries:
result = model.Query(f"EVALUATE {{{query[1]}}}")
assert result == query[0]


@pytest.mark.parametrize("model", testing_parameters)
def test_file_query(model):
singlevaltest = local.SINGLEVALTESTPATH
dfvaltest = local.DFVALTESTPATH
dfdupe = pd.DataFrame({"[Value1]": (1, 3), "[Value2]": (2, 4)})
assert model.Query(singlevaltest) == 1 and model.Query(dfvaltest).equals(dfdupe)


@pytest.mark.parametrize("model", testing_parameters)
def test_repr_str(model):
assert isinstance(model.__repr__(), str)


@pytest.mark.parametrize("model", testing_parameters)
def test_pytables_count(model):
assert model.Tables[testingtablename].Row_Count() > 0


@pytest.mark.parametrize("model", testing_parameters)
def test_pytables_refresh(model):
assert len(model.Tables[testingtablename].Refresh()) > 0


@pytest.mark.parametrize("model", testing_parameters)
def test_pypartitions_refresh(model):
assert len(model.Tables[testingtablename].Partitions[0].Refresh()) > 0


@pytest.mark.parametrize("model", testing_parameters)
def test_pyobjects_adding(model):
table = model.Tables.Find(testingtablename)
table += table
assert len(table) == 2


@pytest.mark.parametrize("model", testing_parameters)
def test_nonetype_decimal_bug(model):
query_str = """
EVALUATE
{
(1, CONVERT( 1.24, CURRENCY ), "Hello"), (2, CONVERT( 87661, CURRENCY ), "World"), (3,,"Test")
}
"""
assert len(model.Query(query_str)) == 3
48 changes: 48 additions & 0 deletions test/test_3custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""These are tests I have for pretty janky features of PyTabular.
These were designed selfishly for my own uses.
So seperating out, to one day sunset and remove.
"""
from test.config import aas, gen2, testingtablename
import pytest

testing_parameters = [pytest.param(aas, id="AAS"), pytest.param(gen2, id="GEN2")]


@pytest.mark.parametrize("model", testing_parameters)
def test_query_every_table(model):
assert len(model.Query_Every_Table()) > 0


@pytest.mark.parametrize("model", testing_parameters)
def test_query_every_column(model):
assert len(model.Query_Every_Column()) > 0


@pytest.mark.parametrize("model", testing_parameters)
def test_backingup_table(model):
model.Backup_Table(testingtablename)
assert (
len(
[
table
for table in model.Model.Tables.GetEnumerator()
if f"{testingtablename}_backup" == table.Name
]
)
== 1
)


@pytest.mark.parametrize("model", testing_parameters)
def test_revert_table(model):
model.Revert_Table(testingtablename)
assert (
len(
[
table
for table in model.Model.Tables.GetEnumerator()
if f"{testingtablename}" == table.Name
]
)
== 1
)
12 changes: 12 additions & 0 deletions test/test_4bpa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from test.config import aas, gen2
import pytest
import pytabular as p

testing_parameters = [pytest.param(aas, id="AAS"), pytest.param(gen2, id="GEN2")]


@pytest.mark.parametrize("model", testing_parameters)
def test_bpa(model):
te2 = p.Tabular_Editor().EXE
bpa = p.BPA().Location
assert isinstance(model.Analyze_BPA(te2, bpa), list)
Loading