From fd2279af5ca4e34c62d74ec7cb45e5caff773ff6 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Thu, 1 May 2025 15:52:47 -0400 Subject: [PATCH 1/2] query formatting poc working --- docs/CONTRIBUTING.md | 20 ++---------- pytabular/currency.py | 66 ++++++++++++++++++++++++++++++++++++++++ pytabular/logic_utils.py | 33 ++++++++++++++++++++ test/test_3tabular.py | 27 ++++++++++++++++ 4 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 pytabular/currency.py diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ac054fa..ae1bd67 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -3,24 +3,10 @@ ## Goal - Make **Python** a first class citizen for interacting with **Tabular models**. -## Some rules -- See [github actions](https://github.com/Curts0/PyTabular/actions) for checks run on pull request. -- We `docstring-coverage` to check for 100% docstring coverage. -- `flake8` also runs, but with a few extra packages. (pep8-naming, flake8-docstrings). -- Updates of any kind are welcome! Even just letting me know of the issues. Or updating docstrings... -- Limit any extra packages, see `pyproject.toml` for dependencies -- Install pre-commit. Pre-commit will run pytest, flake8, and docstr-coverage before push. -```powershell -pip install pre-commit -pre-commit install --hook-type pre-push -``` -- **This will take a while...** pytest will open a PBIX file in repository and run tests on it... Eventually these tests will be run on a model that is not local. +## Info -## Documentation help -- Docstrings follow the google docstring convention. See [Example](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). -- The `flake8-docstrings` will check that google docstring format is followed. -- Docstrings get converted to markdown with the `mkdocstring` package. -- Then gets converted to a site with the `mkdocs` package. +- Use `tox` for local testing. + - Will run docstring coverage, linter, and python versions needed to support w/ pytest. ## Misc - Work will be distributed under a MIT license. \ No newline at end of file diff --git a/pytabular/currency.py b/pytabular/currency.py new file mode 100644 index 0000000..b7c8795 --- /dev/null +++ b/pytabular/currency.py @@ -0,0 +1,66 @@ +"""List of [unicode currencies](https://www.unicode.org/charts/PDF/U20A0.pdf). + +Used to strip formatted values from DAX queries. +For example `$(12,345.67)` will output -> `-12345.67`. +See `logic_utils.clean_formatting` for more. +""" + +unicode_list = [ + "\u0024", + "\u00a2", + "\u00a3", + "\u00a4", + "\u00a5", + "\u0192", + "\u058f", + "\u060b", + "\u09f2", + "\u09f3", + "\u0af1", + "\u0bf9", + "\u0e3f", + "\u17db", + "\u2133", + "\u5143", + "\u5186", + "\u5706", + "\u5713", + "\ufdfc", + # "\u1E2FF", + "\u0024", + "\u20a0", + "\u20a1", + "\u20a2", + "\u20a3", + "\u20a4", + "\u20a5", + "\u20a6", + "\u20a7", + "\u20a8", + "\u20a9", + "\u20aa", + "\u20ab", + "\u20ac", + "\u20ad", + "\u20ae", + "\u20af", + "\u20b0", + "\u20b1", + "\u20b2", + "\u20b3", + "\u20b4", + "\u20b5", + "\u20b6", + "\u20b7", + "\u20b8", + "\u20b9", + "\u20ba", + "\u20bb", + "\u20bc", + "\u20bd", + "\u20be", + "\u20bf", + "\u20c0", +] + +unicodes: str.maketrans = str.maketrans(dict.fromkeys(unicode_list, "")) diff --git a/pytabular/logic_utils.py b/pytabular/logic_utils.py index 58f718a..b77bd93 100644 --- a/pytabular/logic_utils.py +++ b/pytabular/logic_utils.py @@ -5,6 +5,9 @@ import os from typing import Dict, List import pandas as pd + +from pytabular.currency import unicodes + from Microsoft.AnalysisServices.Tabular import DataType from Microsoft.AnalysisServices.AdomdClient import AdomdDataReader @@ -162,6 +165,28 @@ def get_sub_list(lst: list, n: int) -> list: return [lst[i : i + n] for i in range(0, len(lst), n)] +def clean_formatting(query_result: str, sep: str = ",") -> float: + """Attempts to clean DAX formatting. + + For example, `$(12,345.67)` will output -> `-12345.67`. + + Args: + query_result (str): The value given from `get_value_to_df` + sep (str, optional): The thousands separator. Defaults to ",". + + Returns: + float: Value of DAX query cell. + """ + multiplier = 1 + if "(" in query_result and ")" in query_result: + multiplier = -1 + query_result = query_result.replace("(", "").replace(")", "") + + query_result = query_result.replace(sep, "") + + return float(query_result.translate(unicodes)) * multiplier + + def get_value_to_df(query: AdomdDataReader, index: int): """Gets the values from the AdomdDataReader to convert to python df. @@ -171,11 +196,19 @@ def get_value_to_df(query: AdomdDataReader, index: int): query (AdomdDataReader): The AdomdDataReader .Net object. index (int): Index of the value to perform the logic on. """ + # TODO: Clean this up if ( query.GetDataTypeName((index)) in ("Decimal") and query.GetValue(index) is not None ): return query.GetValue(index).ToDouble(query.GetValue(index)) + elif query.GetDataTypeName((index)) in ( + "String" + ): + try: + return clean_formatting(query.GetValue(index)) + except Exception: + return query.GetValue(index) else: return query.GetValue(index) diff --git a/test/test_3tabular.py b/test/test_3tabular.py index 7ac63dc..6987381 100644 --- a/test/test_3tabular.py +++ b/test/test_3tabular.py @@ -27,6 +27,33 @@ def test_datatype_query(model): assert result == query[0] +number_queries = ( + pytest.param(("EVALUATE {1}", 1), id="basic"), + pytest.param( + ("""EVALUATE {FORMAT(12345.67, "$#,##0.00;($#,##0.00)")}""", 12345.67), + id="$#,##0.00", + ), + pytest.param( + ("""EVALUATE {FORMAT(12345.00, "$#,##0.00;($#,##0.00)")}""", 12345.00), + id="$#,##0.00 w/o decimal", + ), + pytest.param( + ("""EVALUATE {FORMAT(-12345.67, "$#,##0.00;($#,##0.00)")}""", -12345.67), + id="$#,##0.00 w/ -", + ), + pytest.param( + ("""EVALUATE {FORMAT(12345.67, "#,##0.00;(#,##0.00)")}""", 12345.67), + id="#,##0.00", + ), +) + + +@pytest.mark.parametrize("number_queries", number_queries) +def test_parsing_query(model, number_queries): + """Assert that the proper query result stripping is happenign.""" + assert model.query(number_queries[0]) == number_queries[1] + + def test_file_query(model): """Test `query()` via a file.""" singlevaltest = get_test_path() + "\\singlevaltest.dax" From 50429c24c3b36d565e407158f1644a2221e1e584 Mon Sep 17 00:00:00 2001 From: Curtis Stallings Date: Thu, 1 May 2025 15:53:05 -0400 Subject: [PATCH 2/2] query poc working --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac031ea..8308258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python_tabular" -version = "0.5.6" +version = "0.5.7" authors = [ { name="Curtis Stallings", email="curtisrstallings@gmail.com" }, ]