-
Notifications
You must be signed in to change notification settings - Fork 91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Timestamptz
autoconversion
#978
base: master
Are you sure you want to change the base?
Changes from all commits
8cfa0c7
95d8e4d
7dd76d8
06e0625
c589d44
fc708fc
705629a
a25e814
86004b0
6611a97
aa9f077
6fcb93f
59a531e
7a3c584
41a6d0c
ad0d1d1
c816d8e
cbf6d41
db53939
de33a9b
36b0da6
9fda29d
3733218
7c6ce55
755b059
026b182
e9b02be
4327178
e9fae63
4416adc
0ab2d24
c64efa9
1d98c36
2c75e70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,7 @@ class Band(Table): | |
from piccolo.querystring import QueryString, Unquoted | ||
from piccolo.utils.encoding import dump_json | ||
from piccolo.utils.warnings import colored_warning | ||
from piccolo.utils.zoneinfo import ZoneInfo | ||
|
||
if t.TYPE_CHECKING: # pragma: no cover | ||
from piccolo.columns.base import ColumnMeta | ||
|
@@ -955,30 +956,33 @@ def __set__(self, obj, value: t.Union[datetime, None]): | |
class Timestamptz(Column): | ||
""" | ||
Used for storing timezone aware datetimes. Uses the ``datetime`` type for | ||
values. The values are converted to UTC in the database, and are also | ||
returned as UTC. | ||
values. The values are converted to UTC when saved into the database and | ||
are converted back into the timezone of the column on select queries. | ||
|
||
**Example** | ||
|
||
.. code-block:: python | ||
|
||
import datetime | ||
from zoneinfo import ZoneInfo | ||
|
||
class Concert(Table): | ||
starts = Timestamptz() | ||
class TallinnConcerts(Table): | ||
event_start = Timestamptz(at_time_zone=ZoneInfo("Europe/Tallinn")) | ||
|
||
# Create | ||
>>> await Concert( | ||
... starts=datetime.datetime( | ||
... year=2050, month=1, day=1, tzinfo=datetime.timezone.tz | ||
>>> await TallinnConcerts( | ||
... event_start=datetime.datetime( | ||
... year=2050, month=1, day=1, hour=20 | ||
... ) | ||
... ).save() | ||
|
||
# Query | ||
>>> await Concert.select(Concert.starts) | ||
>>> await TallinnConcerts.select(TallinnConcerts.event_start) | ||
{ | ||
'starts': datetime.datetime( | ||
2050, 1, 1, 0, 0, tzinfo=datetime.timezone.utc | ||
'event_start': datetime.datetime( | ||
2050, 1, 1, 20, 0, tzinfo=zoneinfo.ZoneInfo( | ||
key='Europe/Tallinn' | ||
) | ||
) | ||
} | ||
|
||
|
@@ -993,22 +997,59 @@ class Concert(Table): | |
timedelta_delegate = TimedeltaDelegate() | ||
|
||
def __init__( | ||
self, default: TimestamptzArg = TimestamptzNow(), **kwargs | ||
self, | ||
default: TimestamptzArg = TimestamptzNow(), | ||
at_time_zone: ZoneInfo = ZoneInfo("UTC"), | ||
**kwargs, | ||
) -> None: | ||
self._validate_default( | ||
default, TimestamptzArg.__args__ # type: ignore | ||
) | ||
|
||
if isinstance(default, datetime): | ||
default = TimestamptzCustom.from_datetime(default) | ||
default = TimestamptzCustom.from_datetime(default, at_time_zone) | ||
|
||
if default == datetime.now: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit confused about this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, it should be the line |
||
default = TimestamptzNow() | ||
default = TimestamptzNow(tz=at_time_zone) | ||
|
||
self._at_time_zone = at_time_zone | ||
self.default = default | ||
kwargs.update({"default": default}) | ||
kwargs.update({"default": default, "at_time_zone": at_time_zone}) | ||
super().__init__(**kwargs) | ||
|
||
########################################################################### | ||
|
||
def at_time_zone(self, time_zone: t.Union[ZoneInfo, str]) -> Timestamptz: | ||
""" | ||
By default, the database returns the value in UTC. This lets us get | ||
the value converted to the specified timezone. | ||
""" | ||
time_zone = ( | ||
ZoneInfo(time_zone) if isinstance(time_zone, str) else time_zone | ||
) | ||
instance = self.copy() | ||
instance._at_time_zone = time_zone | ||
return instance | ||
Comment on lines
+1022
to
+1032
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding #959, it appears that we are creating a new object by copying |
||
|
||
########################################################################### | ||
|
||
def get_select_string( | ||
self, engine_type: str, with_alias: bool = True | ||
) -> str: | ||
select_string = self._meta.get_full_name(with_alias=False) | ||
|
||
if self._at_time_zone != ZoneInfo("UTC"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There might be other instances where timezone comparisons are performed similarly, and I'll use this one as an example. It seems this type of comparison could lead to unexpected outcomes. The following example might illustrate this unpredictability. >>> import datetime
>>> from zoneinfo import ZoneInfo
>>> tz1 = datetime.timezone.utc
>>> tz2 = ZoneInfo("UTC")
>>> d1 = datetime.datetime(year=2024, month=4, day=7, tzinfo=tz1)
>>> d2 = datetime.datetime(year=2024, month=4, day=7, tzinfo=tz2)
>>> d1 == d2
True
>>> str(d1) == str(d2)
True
>>> d1.tzinfo, d2.tzinfo
(datetime.timezone.utc, zoneinfo.ZoneInfo(key='UTC'))
>>> d1.tzinfo == d2.tzinfo
False
>>> d1.tzinfo is d2.tzinfo
False |
||
# SQLite doesn't support `AT TIME ZONE`, so we have to do it in | ||
# Python instead (see ``Select.response_handler``). | ||
if self._meta.engine_type in ("postgres", "cockroach"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Occurrences of |
||
select_string += f" AT TIME ZONE '{self._at_time_zone.key}'" | ||
|
||
if with_alias: | ||
alias = self._get_alias() | ||
select_string += f' AS "{alias}"' | ||
|
||
return select_string | ||
|
||
########################################################################### | ||
# For update queries | ||
|
||
|
@@ -2305,7 +2346,7 @@ def arrow(self, key: str) -> JSONB: | |
Allows part of the JSON structure to be returned - for example, | ||
for {"a": 1}, and a key value of "a", then 1 will be returned. | ||
""" | ||
instance = t.cast(JSONB, self.copy()) | ||
instance = self.copy() | ||
instance.json_operator = f"-> '{key}'" | ||
return instance | ||
|
||
|
@@ -2318,7 +2359,7 @@ def get_select_string( | |
select_string += f" {self.json_operator}" | ||
|
||
if with_alias: | ||
alias = self._alias or self._meta.get_default_alias() | ||
alias = self._get_alias() | ||
select_string += f' AS "{alias}"' | ||
|
||
return select_string | ||
|
@@ -2623,7 +2664,7 @@ def get_select_string(self, engine_type: str, with_alias=True) -> str: | |
select_string += f"[{self.index}]" | ||
|
||
if with_alias: | ||
alias = self._alias or self._meta.get_default_alias() | ||
alias = self._get_alias() | ||
select_string += f' AS "{alias}"' | ||
|
||
return select_string | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that
tzinfo=datetime.timezone.tz
should be replaced withtzinfo=datetime.timezone.utc
in the old documentation.