Skip to content
Open
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
27 changes: 27 additions & 0 deletions tableauserverclient/server/filter.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import datetime

from tableauserverclient.datetime_helpers import format_datetime
from .request_options import RequestOptions


Expand All @@ -9,6 +12,30 @@ def __init__(self, field, operator, value):
self.value = value

def __str__(self):
"""Return the filter as a Tableau REST API filter string.

Format: ``<field>:<operator>:<value>``

Value serialization rules:
- datetime: ISO-8601 UTC, e.g. ``2023-01-01T00:00:00Z``.
Naive datetimes (no tzinfo) are rejected with ValueError; always
pass timezone-aware datetime objects.
- bool: lowercase ``true`` or ``false`` as required by the REST API.
- list: bracket-enclosed comma-separated values, e.g. ``[a,b,c]``.
Only valid with the ``in`` operator.
- All other types: ``str()`` of the value.
"""
if isinstance(self._value, datetime.datetime):
if self._value.tzinfo is None:
raise ValueError(
"Naive datetime passed to Filter; Tableau Server requires UTC. "
"Use a timezone-aware datetime, e.g. "
"datetime.datetime(..., tzinfo=datetime.timezone.utc)."
)
return f"{self.field}:{self.operator}:{format_datetime(self._value)}"
if isinstance(self._value, bool):
# Tableau REST API requires lowercase 'true'/'false', not Python's 'True'/'False'
return f"{self.field}:{self.operator}:{str(self._value).lower()}"
value_string = str(self._value)
if isinstance(self._value, list):
# this should turn the string representation of the list
Expand Down
109 changes: 109 additions & 0 deletions test/test_filter.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

import tableauserverclient as TSC


Expand All @@ -14,3 +16,110 @@ def test_filter_in():
filter = TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.In, projects_to_find)

assert str(filter) == "name:in:[default,Salesforce Sales Projeśt]"


def test_filter_in_single_value():
"""A single-element list produces valid bracket syntax."""
filter = TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["sample"])

assert str(filter) == "tags:in:[sample]"


def test_filter_in_multiple_values():
"""Multi-element list produces comma-separated values inside brackets."""
filter = TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["a", "b", "c"])

assert str(filter) == "tags:in:[a,b,c]"


def test_filter_integer_value():
"""Integer filter values are serialized as plain decimal strings."""
filter = TSC.Filter(TSC.RequestOptions.Field.Size, TSC.RequestOptions.Operator.GreaterThan, 0)

assert str(filter) == "size:gt:0"


def test_filter_integer_nonzero():
filter = TSC.Filter(TSC.RequestOptions.Field.SheetCount, TSC.RequestOptions.Operator.GreaterThanOrEqual, 5)

assert str(filter) == "sheetCount:gte:5"


def test_filter_date_no_encoding():
"""Date filter values should not have colons pre-encoded (fixes #1025).

The requests library handles URL encoding of the whole filter parameter,
so pre-encoding colons in datetime values causes double-encoding on the wire.
"""
utc = datetime.timezone.utc
dt = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=utc)
filter = TSC.Filter(TSC.RequestOptions.Field.CreatedAt, TSC.RequestOptions.Operator.LessThan, dt)

result = str(filter)
assert result == "createdAt:lt:2023-01-01T00:00:00Z"
assert "%3A" not in result, "Colons in datetime values must not be percent-encoded"


def test_filter_date_uses_tableau_format():
"""Datetime values are serialized in Tableau ISO-8601 format, not Python default."""
utc = datetime.timezone.utc
dt = datetime.datetime(2024, 6, 15, 12, 30, 45, tzinfo=utc)
filter = TSC.Filter(TSC.RequestOptions.Field.UpdatedAt, TSC.RequestOptions.Operator.GreaterThan, dt)

result = str(filter)
# Must use 'T' separator and 'Z' suffix, not Python's space-separated format
assert result == "updatedAt:gt:2024-06-15T12:30:45Z"
assert " " not in result.split(":", 2)[2], "Datetime value must not contain a space (Python default format)"


def test_filter_date_non_utc_converted():
"""Non-UTC datetime values are converted to UTC before serialization."""
eastern = datetime.timezone(datetime.timedelta(hours=-5))
dt = datetime.datetime(2023, 3, 10, 12, 0, 0, tzinfo=eastern)
filter = TSC.Filter(TSC.RequestOptions.Field.CreatedAt, TSC.RequestOptions.Operator.Equals, dt)

result = str(filter)
assert result == "createdAt:eq:2023-03-10T17:00:00Z"


def test_filter_bool_true():
"""Boolean True is serialized as lowercase 'true' for Tableau REST API."""
filter = TSC.Filter(TSC.RequestOptions.Field.IsCertified, TSC.RequestOptions.Operator.Equals, True)

result = str(filter)
assert result == "isCertified:eq:true"
assert "True" not in result, "Boolean True must be lowercase 'true'"


def test_filter_bool_false():
"""Boolean False is serialized as lowercase 'false' for Tableau REST API."""
filter = TSC.Filter(TSC.RequestOptions.Field.IsCertified, TSC.RequestOptions.Operator.Equals, False)

result = str(filter)
assert result == "isCertified:eq:false"
assert "False" not in result, "Boolean False must be lowercase 'false'"


def test_filter_bool_has_extracts():
"""Boolean filter works for hasExtracts field."""
filter = TSC.Filter(TSC.RequestOptions.Field.HasExtracts, TSC.RequestOptions.Operator.Equals, True)

assert str(filter) == "hasExtracts:eq:true"


def test_filter_date_naive_raises():
"""A naive datetime (no tzinfo) raises ValueError with a helpful message."""
import pytest

naive_dt = datetime.datetime(2023, 1, 1, 12, 0, 0) # no tzinfo
f = TSC.Filter(TSC.RequestOptions.Field.CreatedAt, TSC.RequestOptions.Operator.Equals, naive_dt)
with pytest.raises(ValueError, match="Naive datetime"):
str(f)


def test_filter_list_rejects_non_in_operator():
"""A list value with a non-In operator raises ValueError."""
import pytest

with pytest.raises(ValueError):
TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.Equals, ["a", "b"])
Loading