Skip to content

Commit 0acb5e7

Browse files
committed
Add support for TIMEZONE
1 parent 3b154b7 commit 0acb5e7

File tree

7 files changed

+138
-3
lines changed

7 files changed

+138
-3
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,20 @@ conn = trino.dbapi.connect(
362362
)
363363
```
364364

365+
## Timezone
366+
367+
The time zone for the session can be explicitly set using the official IANA time zone name. When not set the time zone defaults to the client side local timezone.
368+
369+
```python
370+
import trino
371+
conn = trino.dbapi.connect(
372+
host='localhost',
373+
port=443,
374+
user='username',
375+
timezone="Europe/Brussels",
376+
)
377+
```
378+
365379
## SSL
366380

367381
### SSL verification

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@
7979
"Programming Language :: Python :: Implementation :: PyPy",
8080
"Topic :: Database :: Front-Ends",
8181
],
82-
python_requires=">=3.7",
83-
install_requires=["pytz", "requests"],
82+
python_requires='>=3.7',
83+
install_requires=["pytz", "requests", "tzlocal"],
8484
extras_require={
8585
"all": all_require,
8686
"kerberos": kerberos_require,

tests/integration/test_dbapi_integration.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import pytest
1717
import pytz
1818
import requests
19+
from tzlocal import get_localzone_name # type: ignore
1920

2021
import trino
2122
from tests.integration.conftest import trino_version
@@ -1105,3 +1106,31 @@ def test_prepared_statements(run_trino):
11051106
cur.execute('DEALLOCATE PREPARE test_prepared_statements')
11061107
cur.fetchall()
11071108
assert cur._request._client_session.prepared_statements == {}
1109+
1110+
1111+
def test_set_timezone_in_connection(run_trino):
1112+
_, host, port = run_trino
1113+
1114+
trino_connection = trino.dbapi.Connection(
1115+
host=host, port=port, user="test", catalog="tpch", timezone="Europe/Brussels"
1116+
)
1117+
cur = trino_connection.cursor()
1118+
cur.execute('SELECT current_timezone()')
1119+
res = cur.fetchall()
1120+
assert res[0][0] == "Europe/Brussels"
1121+
1122+
1123+
def test_connection_without_timezone(run_trino):
1124+
_, host, port = run_trino
1125+
1126+
trino_connection = trino.dbapi.Connection(
1127+
host=host, port=port, user="test", catalog="tpch"
1128+
)
1129+
cur = trino_connection.cursor()
1130+
cur.execute('SELECT current_timezone()')
1131+
res = cur.fetchall()
1132+
session_tz = res[0][0]
1133+
localzone = get_localzone_name()
1134+
assert session_tz == localzone or \
1135+
(session_tz == "UTC" and localzone == "Etc/UTC") \
1136+
# Workaround for difference between Trino timezone and tzlocal for UTC

tests/unit/test_client.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import requests
2323
from httpretty import httprettified
2424
from requests_kerberos.exceptions import KerberosExchangeError
25+
from tzlocal import get_localzone_name # type: ignore
2526

2627
import trino.exceptions
2728
from tests.unit.oauth_test_utils import (
@@ -47,6 +48,12 @@
4748
_RetryWithExponentialBackoff,
4849
)
4950

51+
try:
52+
from zoneinfo._common import ZoneInfoNotFoundError # type: ignore
53+
54+
except ModuleNotFoundError:
55+
from backports.zoneinfo._common import ZoneInfoNotFoundError # type: ignore
56+
5057

5158
@mock.patch("trino.client.TrinoRequest.http")
5259
def test_trino_initial_request(mock_requests, sample_post_response_data):
@@ -80,6 +87,7 @@ def test_request_headers(mock_get_and_post):
8087
schema = "test_schema"
8188
user = "test_user"
8289
source = "test_source"
90+
timezone = "Europe/Brussels"
8391
accept_encoding_header = "accept-encoding"
8492
accept_encoding_value = "identity,deflate,gzip"
8593
client_info_header = constants.HEADER_CLIENT_INFO
@@ -93,6 +101,7 @@ def test_request_headers(mock_get_and_post):
93101
source=source,
94102
catalog=catalog,
95103
schema=schema,
104+
timezone=timezone,
96105
headers={
97106
accept_encoding_header: accept_encoding_value,
98107
client_info_header: client_info_value,
@@ -108,9 +117,10 @@ def assert_headers(headers):
108117
assert headers[constants.HEADER_SOURCE] == source
109118
assert headers[constants.HEADER_USER] == user
110119
assert headers[constants.HEADER_SESSION] == ""
120+
assert headers[constants.HEADER_TIMEZONE] == timezone
111121
assert headers[accept_encoding_header] == accept_encoding_value
112122
assert headers[client_info_header] == client_info_value
113-
assert len(headers.keys()) == 8
123+
assert len(headers.keys()) == 9
114124

115125
req.post("URL")
116126
_, post_kwargs = post.call_args
@@ -1071,3 +1081,62 @@ def test_request_headers_role_empty(mock_get_and_post):
10711081
req.get("URL")
10721082
_, get_kwargs = get.call_args
10731083
assert_headers_with_roles(post_kwargs["headers"], None)
1084+
1085+
1086+
def assert_headers_timezone(headers: Dict[str, str], timezone: str):
1087+
assert headers[constants.HEADER_TIMEZONE] == timezone
1088+
1089+
1090+
def test_request_headers_with_timezone(mock_get_and_post):
1091+
get, post = mock_get_and_post
1092+
1093+
req = TrinoRequest(
1094+
host="coordinator",
1095+
port=8080,
1096+
client_session=ClientSession(
1097+
user="test_user",
1098+
timezone="Europe/Brussels"
1099+
),
1100+
)
1101+
1102+
req.post("URL")
1103+
_, post_kwargs = post.call_args
1104+
assert_headers_timezone(post_kwargs["headers"], "Europe/Brussels")
1105+
1106+
req.get("URL")
1107+
_, get_kwargs = get.call_args
1108+
assert_headers_timezone(post_kwargs["headers"], "Europe/Brussels")
1109+
1110+
1111+
def test_request_headers_without_timezone(mock_get_and_post):
1112+
get, post = mock_get_and_post
1113+
1114+
req = TrinoRequest(
1115+
host="coordinator",
1116+
port=8080,
1117+
client_session=ClientSession(
1118+
user="test_user",
1119+
),
1120+
)
1121+
localzone = get_localzone_name()
1122+
1123+
req.post("URL")
1124+
_, post_kwargs = post.call_args
1125+
assert_headers_timezone(post_kwargs["headers"], localzone)
1126+
1127+
req.get("URL")
1128+
_, get_kwargs = get.call_args
1129+
assert_headers_timezone(post_kwargs["headers"], localzone)
1130+
1131+
1132+
def test_request_with_invalid_timezone(mock_get_and_post):
1133+
with pytest.raises(ZoneInfoNotFoundError) as zinfo_error:
1134+
TrinoRequest(
1135+
host="coordinator",
1136+
port=8080,
1137+
client_session=ClientSession(
1138+
user="test_user",
1139+
timezone="INVALID_TIMEZONE"
1140+
),
1141+
)
1142+
assert str(zinfo_error.value).startswith("'No time zone found with key")

trino/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,18 @@
4747

4848
import pytz
4949
import requests
50+
from tzlocal import get_localzone_name # type: ignore
5051

5152
import trino.logging
5253
from trino import constants, exceptions
5354

55+
try:
56+
from zoneinfo import ZoneInfo # type: ignore
57+
58+
except ModuleNotFoundError:
59+
from backports.zoneinfo import ZoneInfo # type: ignore
60+
61+
5462
__all__ = ["ClientSession", "TrinoQuery", "TrinoRequest", "PROXIES"]
5563

5664
logger = trino.logging.get_logger(__name__)
@@ -100,6 +108,8 @@ class ClientSession(object):
100108
:param client_tags: Client tags as list of strings.
101109
:param roles: roles for the current session. Some connectors do not
102110
support role management. See connector documentation for more details.
111+
:param timezone: The timezone for query processing. Defaults to the timezone
112+
of the Trino cluster, and not the timezone of the client.
103113
"""
104114

105115
def __init__(
@@ -114,6 +124,7 @@ def __init__(
114124
extra_credential: List[Tuple[str, str]] = None,
115125
client_tags: List[str] = None,
116126
roles: Dict[str, str] = None,
127+
timezone: str = None,
117128
):
118129
self._user = user
119130
self._catalog = catalog
@@ -127,6 +138,9 @@ def __init__(
127138
self._roles = roles.copy() if roles is not None else {}
128139
self._prepared_statements: Dict[str, str] = {}
129140
self._object_lock = threading.Lock()
141+
if timezone: # Check timezone validity
142+
ZoneInfo(timezone)
143+
self._timezone = timezone or get_localzone_name()
130144

131145
@property
132146
def user(self):
@@ -207,6 +221,11 @@ def prepared_statements(self, prepared_statements):
207221
with self._object_lock:
208222
self._prepared_statements = prepared_statements
209223

224+
@property
225+
def timezone(self):
226+
with self._object_lock:
227+
return self._timezone
228+
210229
def __getstate__(self):
211230
state = self.__dict__.copy()
212231
del state["_object_lock"]
@@ -408,6 +427,7 @@ def http_headers(self) -> Dict[str, str]:
408427
headers[constants.HEADER_SCHEMA] = self._client_session.schema
409428
headers[constants.HEADER_SOURCE] = self._client_session.source
410429
headers[constants.HEADER_USER] = self._client_session.user
430+
headers[constants.HEADER_TIMEZONE] = self._client_session.timezone
411431
if len(self._client_session.roles.values()):
412432
headers[constants.HEADER_ROLE] = ",".join(
413433
# ``name`` must not contain ``=``

trino/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
HEADER_CLIENT_INFO = "X-Trino-Client-Info"
3434
HEADER_CLIENT_TAGS = "X-Trino-Client-Tags"
3535
HEADER_EXTRA_CREDENTIAL = "X-Trino-Extra-Credential"
36+
HEADER_TIMEZONE = "X-Trino-Time-Zone"
3637

3738
HEADER_SESSION = "X-Trino-Session"
3839
HEADER_SET_SESSION = "X-Trino-Set-Session"

trino/dbapi.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__(
110110
client_tags=None,
111111
experimental_python_types=False,
112112
roles=None,
113+
timezone=None,
113114
):
114115
self.host = host
115116
self.port = port
@@ -129,6 +130,7 @@ def __init__(
129130
extra_credential=extra_credential,
130131
client_tags=client_tags,
131132
roles=roles,
133+
timezone=timezone,
132134
)
133135
# mypy cannot follow module import
134136
if http_session is None:

0 commit comments

Comments
 (0)