Skip to content

Commit 03f2670

Browse files
ulisesojedahashhar
andcommitted
Add support for TIMEZONE
Co-authored-by: Ulises <[email protected]> Co-authored-by: Ashhar Hasan <[email protected]>
1 parent 18b826d commit 03f2670

File tree

7 files changed

+140
-3
lines changed

7 files changed

+140
-3
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,24 @@ conn = trino.dbapi.connect(
362362
)
363363
```
364364

365+
## Timezone
366+
367+
The time zone for the session can be explicitly set using the IANA time zone
368+
name. When not set the time zone defaults to the client side local timezone.
369+
370+
```python
371+
import trino
372+
conn = trino.dbapi.connect(
373+
host='localhost',
374+
port=443,
375+
user='username',
376+
timezone='Europe/Brussels',
377+
)
378+
```
379+
380+
> **NOTE: The behaviour till version 0.320.0 was the same as setting session timezone to UTC.**
381+
> **To preserve that behaviour pass `timezone='UTC'` when creating the connection.**
382+
365383
## SSL
366384

367385
### SSL verification

setup.py

+2-2
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

+29
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
@@ -1107,3 +1108,31 @@ def test_prepared_statements(run_trino):
11071108
cur.execute('DEALLOCATE PREPARE test_prepared_statements')
11081109
cur.fetchall()
11091110
assert cur._request._client_session.prepared_statements == {}
1111+
1112+
1113+
def test_set_timezone_in_connection(run_trino):
1114+
_, host, port = run_trino
1115+
1116+
trino_connection = trino.dbapi.Connection(
1117+
host=host, port=port, user="test", catalog="tpch", timezone="Europe/Brussels"
1118+
)
1119+
cur = trino_connection.cursor()
1120+
cur.execute('SELECT current_timezone()')
1121+
res = cur.fetchall()
1122+
assert res[0][0] == "Europe/Brussels"
1123+
1124+
1125+
def test_connection_without_timezone(run_trino):
1126+
_, host, port = run_trino
1127+
1128+
trino_connection = trino.dbapi.Connection(
1129+
host=host, port=port, user="test", catalog="tpch"
1130+
)
1131+
cur = trino_connection.cursor()
1132+
cur.execute('SELECT current_timezone()')
1133+
res = cur.fetchall()
1134+
session_tz = res[0][0]
1135+
localzone = get_localzone_name()
1136+
assert session_tz == localzone or \
1137+
(session_tz == "UTC" and localzone == "Etc/UTC") \
1138+
# Workaround for difference between Trino timezone and tzlocal for UTC

tests/unit/test_client.py

+69-1
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 (
@@ -48,6 +49,11 @@
4849
_RetryWithExponentialBackoff,
4950
)
5051

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

5258
@mock.patch("trino.client.TrinoRequest.http")
5359
def test_trino_initial_request(mock_requests, sample_post_response_data):
@@ -81,6 +87,7 @@ def test_request_headers(mock_get_and_post):
8187
schema = "test_schema"
8288
user = "test_user"
8389
source = "test_source"
90+
timezone = "Europe/Brussels"
8491
accept_encoding_header = "accept-encoding"
8592
accept_encoding_value = "identity,deflate,gzip"
8693
client_info_header = constants.HEADER_CLIENT_INFO
@@ -94,6 +101,7 @@ def test_request_headers(mock_get_and_post):
94101
source=source,
95102
catalog=catalog,
96103
schema=schema,
104+
timezone=timezone,
97105
headers={
98106
accept_encoding_header: accept_encoding_value,
99107
client_info_header: client_info_value,
@@ -109,9 +117,10 @@ def assert_headers(headers):
109117
assert headers[constants.HEADER_SOURCE] == source
110118
assert headers[constants.HEADER_USER] == user
111119
assert headers[constants.HEADER_SESSION] == ""
120+
assert headers[constants.HEADER_TIMEZONE] == timezone
112121
assert headers[accept_encoding_header] == accept_encoding_value
113122
assert headers[client_info_header] == client_info_value
114-
assert len(headers.keys()) == 8
123+
assert len(headers.keys()) == 9
115124

116125
req.post("URL")
117126
_, post_kwargs = post.call_args
@@ -1113,3 +1122,62 @@ def test_request_headers_role_empty(mock_get_and_post):
11131122
req.get("URL")
11141123
_, get_kwargs = get.call_args
11151124
assert_headers_with_roles(post_kwargs["headers"], None)
1125+
1126+
1127+
def assert_headers_timezone(headers: Dict[str, str], timezone: str):
1128+
assert headers[constants.HEADER_TIMEZONE] == timezone
1129+
1130+
1131+
def test_request_headers_with_timezone(mock_get_and_post):
1132+
get, post = mock_get_and_post
1133+
1134+
req = TrinoRequest(
1135+
host="coordinator",
1136+
port=8080,
1137+
client_session=ClientSession(
1138+
user="test_user",
1139+
timezone="Europe/Brussels"
1140+
),
1141+
)
1142+
1143+
req.post("URL")
1144+
_, post_kwargs = post.call_args
1145+
assert_headers_timezone(post_kwargs["headers"], "Europe/Brussels")
1146+
1147+
req.get("URL")
1148+
_, get_kwargs = get.call_args
1149+
assert_headers_timezone(post_kwargs["headers"], "Europe/Brussels")
1150+
1151+
1152+
def test_request_headers_without_timezone(mock_get_and_post):
1153+
get, post = mock_get_and_post
1154+
1155+
req = TrinoRequest(
1156+
host="coordinator",
1157+
port=8080,
1158+
client_session=ClientSession(
1159+
user="test_user",
1160+
),
1161+
)
1162+
localzone = get_localzone_name()
1163+
1164+
req.post("URL")
1165+
_, post_kwargs = post.call_args
1166+
assert_headers_timezone(post_kwargs["headers"], localzone)
1167+
1168+
req.get("URL")
1169+
_, get_kwargs = get.call_args
1170+
assert_headers_timezone(post_kwargs["headers"], localzone)
1171+
1172+
1173+
def test_request_with_invalid_timezone(mock_get_and_post):
1174+
with pytest.raises(ZoneInfoNotFoundError) as zinfo_error:
1175+
TrinoRequest(
1176+
host="coordinator",
1177+
port=8080,
1178+
client_session=ClientSession(
1179+
user="test_user",
1180+
timezone="INVALID_TIMEZONE"
1181+
),
1182+
)
1183+
assert str(zinfo_error.value).startswith("'No time zone found with key")

trino/client.py

+19
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,18 @@
5050
import pytz
5151
import requests
5252
from pytz.tzinfo import BaseTzInfo
53+
from tzlocal import get_localzone_name # type: ignore
5354

5455
import trino.logging
5556
from trino import constants, exceptions
5657

58+
try:
59+
from zoneinfo import ZoneInfo # type: ignore
60+
61+
except ModuleNotFoundError:
62+
from backports.zoneinfo import ZoneInfo # type: ignore
63+
64+
5765
__all__ = ["ClientSession", "TrinoQuery", "TrinoRequest", "PROXIES"]
5866

5967
logger = trino.logging.get_logger(__name__)
@@ -107,6 +115,7 @@ class ClientSession(object):
107115
:param client_tags: Client tags as list of strings.
108116
:param roles: roles for the current session. Some connectors do not
109117
support role management. See connector documentation for more details.
118+
:param timezone: The timezone for query processing. Defaults to the system's local timezone.
110119
"""
111120

112121
def __init__(
@@ -121,6 +130,7 @@ def __init__(
121130
extra_credential: List[Tuple[str, str]] = None,
122131
client_tags: List[str] = None,
123132
roles: Dict[str, str] = None,
133+
timezone: str = None,
124134
):
125135
self._user = user
126136
self._catalog = catalog
@@ -134,6 +144,9 @@ def __init__(
134144
self._roles = roles.copy() if roles is not None else {}
135145
self._prepared_statements: Dict[str, str] = {}
136146
self._object_lock = threading.Lock()
147+
self._timezone = timezone or get_localzone_name()
148+
if timezone: # Check timezone validity
149+
ZoneInfo(timezone)
137150

138151
@property
139152
def user(self):
@@ -214,6 +227,11 @@ def prepared_statements(self, prepared_statements):
214227
with self._object_lock:
215228
self._prepared_statements = prepared_statements
216229

230+
@property
231+
def timezone(self):
232+
with self._object_lock:
233+
return self._timezone
234+
217235
def __getstate__(self):
218236
state = self.__dict__.copy()
219237
del state["_object_lock"]
@@ -415,6 +433,7 @@ def http_headers(self) -> Dict[str, str]:
415433
headers[constants.HEADER_SCHEMA] = self._client_session.schema
416434
headers[constants.HEADER_SOURCE] = self._client_session.source
417435
headers[constants.HEADER_USER] = self._client_session.user
436+
headers[constants.HEADER_TIMEZONE] = self._client_session.timezone
418437
if len(self._client_session.roles.values()):
419438
headers[constants.HEADER_ROLE] = ",".join(
420439
# ``name`` must not contain ``=``

trino/constants.py

+1
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

+2
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)