From 8cfa0c75ee84d18900d5a3f866a0155a40721ea8 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Mon, 18 Mar 2024 01:57:44 +0200 Subject: [PATCH 01/19] Added column tz autoconversion to Table __init__ method --- piccolo/table.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/piccolo/table.py b/piccolo/table.py index d6735ac38..67d4eff26 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -5,6 +5,8 @@ import types import typing as t import warnings +from datetime import datetime +from zoneinfo import ZoneInfo from dataclasses import dataclass, field from piccolo.columns import Column @@ -17,6 +19,7 @@ ReferencedTable, Secret, Serial, + Timestamptz, ) from piccolo.columns.defaults.base import Default from piccolo.columns.indexes import IndexMethod @@ -436,6 +439,9 @@ def __init__( ): raise ValueError(f"{column._meta.name} wasn't provided") + if isinstance(column, Timestamptz) and isinstance(value, datetime): + value = value.astimezone(column.tz) + self[column._meta.name] = value unrecognized = kwargs.keys() From 95d8e4dd2e6b3de13236a61b3b5fee378260b965 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Mon, 18 Mar 2024 02:04:55 +0200 Subject: [PATCH 02/19] Added tz ability to Timestamptz --- piccolo/columns/column_types.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index 7012355d7..706db2cb0 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -33,6 +33,7 @@ class Band(Table): import uuid from dataclasses import dataclass from datetime import date, datetime, time, timedelta +from zoneinfo import ZoneInfo from enum import Enum from piccolo.columns.base import ( @@ -1007,6 +1008,7 @@ class Concert(Table): """ value_type = datetime + tz_type = ZoneInfo # Currently just used by ModelBuilder, to know that we want a timezone # aware datetime. @@ -1015,7 +1017,10 @@ class Concert(Table): timedelta_delegate = TimedeltaDelegate() def __init__( - self, default: TimestamptzArg = TimestamptzNow(), **kwargs + self, + tz: ZoneInfo = ZoneInfo('UTC'), + default: TimestamptzArg = TimestamptzNow(), + **kwargs ) -> None: self._validate_default( default, TimestamptzArg.__args__ # type: ignore @@ -1025,10 +1030,11 @@ def __init__( default = TimestamptzCustom.from_datetime(default) if default == datetime.now: - default = TimestamptzNow() + default = TimestamptzNow(tz) + self.tz = tz self.default = default - kwargs.update({"default": default}) + kwargs.update({"tz": tz, "default": default}) super().__init__(**kwargs) ########################################################################### From 7dd76d8c959adeb31c718e6e85a32fef2a560925 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Mon, 18 Mar 2024 02:10:01 +0200 Subject: [PATCH 03/19] Added tz ability to TimestamptzNow --- piccolo/columns/defaults/timestamptz.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index 5db6ebd54..1c40b9c83 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -3,6 +3,7 @@ import datetime import typing as t from enum import Enum +from zoneinfo import ZoneInfo from .timestamp import TimestampCustom, TimestampNow, TimestampOffset @@ -27,12 +28,15 @@ def python(self): class TimestamptzNow(TimestampNow): + def __init__(self, tz: ZoneInfo = ZoneInfo('UTC')): + self._tz = tz + @property def cockroach(self): return "current_timestamp" def python(self): - return datetime.datetime.now(tz=datetime.timezone.utc) + return datetime.datetime.now(tz=self._tz) class TimestamptzCustom(TimestampCustom): From 06e062555e2fe36674e4d3b02a79bed8e7872f4d Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:27:16 +0200 Subject: [PATCH 04/19] Added timezone awareness to TimestamptzOffset and TimestamptzCustom --- piccolo/columns/defaults/timestamptz.py | 36 ++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index 1c40b9c83..277f9aad9 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -9,6 +9,20 @@ class TimestamptzOffset(TimestampOffset): + def __init__( + self, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: int = 0, + tz: ZoneInfo = ZoneInfo('UTC') + ): + self._tz = tz + super().__init__(**{ + k: v for k, v in locals().items() + if k not in ['self', 'tz'] + }) + @property def cockroach(self): interval_string = self.get_postgres_interval_string( @@ -18,7 +32,7 @@ def cockroach(self): def python(self): return datetime.datetime.now( - tz=datetime.timezone.utc + tz=self._tz ) + datetime.timedelta( days=self.days, hours=self.hours, @@ -40,6 +54,22 @@ def python(self): class TimestamptzCustom(TimestampCustom): + def __init__( + self, + year: int = 2000, + month: int = 1, + day: int = 1, + hour: int = 0, + second: int = 0, + microsecond: int = 0, + tz: ZoneInfo = ZoneInfo('UTC') + ): + self._tz = tz + super().__init__(**{ + k: v for k, v in locals().items() + if k not in ['self', 'tz'] + }) + @property def cockroach(self): return "'{}'".format(self.datetime.isoformat().replace("T", " ")) @@ -53,13 +83,13 @@ def datetime(self): hour=self.hour, second=self.second, microsecond=self.microsecond, - tzinfo=datetime.timezone.utc, + tzinfo=self._tz, ) @classmethod def from_datetime(cls, instance: datetime.datetime): # type: ignore if instance.tzinfo is not None: - instance = instance.astimezone(datetime.timezone.utc) + instance = instance.astimezone(self._tz) return cls( year=instance.year, month=instance.month, From c589d44e5842627e30c32e260280b4d49954e431 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:31:00 +0200 Subject: [PATCH 05/19] Added zoneinfo fallback dependency for missing zone data --- requirements/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0a5ee6244..e09e7669a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,3 +5,4 @@ targ>=0.3.7 inflection>=0.5.1 typing-extensions>=4.3.0 pydantic[email]==2.* +tzdata>=2024.1 From fc708fcfc96b1458fedcbc3b338e85333614d7ea Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:35:28 +0200 Subject: [PATCH 06/19] Added backports.zoneinfo --- requirements/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e09e7669a..73c8f4e66 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,3 +6,4 @@ inflection>=0.5.1 typing-extensions>=4.3.0 pydantic[email]==2.* tzdata>=2024.1 +backports.zoneinfo>=0.2.1 From 705629a4a2f5958b6f8f06a14c6e702aa31ec423 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:39:05 +0200 Subject: [PATCH 07/19] Un-privatized tz attribute from default timestamptz classes --- piccolo/columns/defaults/timestamptz.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index 277f9aad9..e52d4445c 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -17,7 +17,7 @@ def __init__( seconds: int = 0, tz: ZoneInfo = ZoneInfo('UTC') ): - self._tz = tz + self.tz = tz super().__init__(**{ k: v for k, v in locals().items() if k not in ['self', 'tz'] @@ -32,7 +32,7 @@ def cockroach(self): def python(self): return datetime.datetime.now( - tz=self._tz + tz=self.tz ) + datetime.timedelta( days=self.days, hours=self.hours, @@ -43,14 +43,14 @@ def python(self): class TimestamptzNow(TimestampNow): def __init__(self, tz: ZoneInfo = ZoneInfo('UTC')): - self._tz = tz + self.tz = tz @property def cockroach(self): return "current_timestamp" def python(self): - return datetime.datetime.now(tz=self._tz) + return datetime.datetime.now(tz=self.tz) class TimestamptzCustom(TimestampCustom): @@ -64,7 +64,7 @@ def __init__( microsecond: int = 0, tz: ZoneInfo = ZoneInfo('UTC') ): - self._tz = tz + self.tz = tz super().__init__(**{ k: v for k, v in locals().items() if k not in ['self', 'tz'] @@ -83,13 +83,13 @@ def datetime(self): hour=self.hour, second=self.second, microsecond=self.microsecond, - tzinfo=self._tz, + tzinfo=self.tz, ) @classmethod def from_datetime(cls, instance: datetime.datetime): # type: ignore if instance.tzinfo is not None: - instance = instance.astimezone(self._tz) + instance = instance.astimezone(self.tz) return cls( year=instance.year, month=instance.month, From a25e814c8338dc650eb5cc8cefce35ea4908c76f Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:52:53 +0200 Subject: [PATCH 08/19] Added python version constraint to backports.zoneinfo --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 73c8f4e66..9199ab8e9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,4 +6,4 @@ inflection>=0.5.1 typing-extensions>=4.3.0 pydantic[email]==2.* tzdata>=2024.1 -backports.zoneinfo>=0.2.1 +backports.zoneinfo>=0.2.1; python_version <= '3.8' From 6611a975f55fd0676c13776bd3d5b22f0e41d909 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:38:37 +0200 Subject: [PATCH 09/19] Updated Timestamptz docstring and fixed a bug in TimestamptzCustom.from_datetime --- piccolo/columns/column_types.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index 6292988f4..ead6237f4 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -978,30 +978,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(tz=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' + ) ) } @@ -1027,7 +1030,7 @@ def __init__( ) if isinstance(default, datetime): - default = TimestamptzCustom.from_datetime(default) + default = TimestamptzCustom.from_datetime(default, tz) if default == datetime.now: default = TimestamptzNow(tz) From aa9f077b31138fb09b3e86ade6070f7dabc2682d Mon Sep 17 00:00:00 2001 From: Mattias Aabmets <6948036+aabmets@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:41:46 +0200 Subject: [PATCH 10/19] Fixed bug in TimestamptzCustom.from_datetime --- piccolo/columns/defaults/timestamptz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index e52d4445c..0edbf3e80 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -87,9 +87,9 @@ def datetime(self): ) @classmethod - def from_datetime(cls, instance: datetime.datetime): # type: ignore + def from_datetime(cls, instance: datetime.datetime, tz: ZoneInfo = ZoneInfo('UTC')): # type: ignore if instance.tzinfo is not None: - instance = instance.astimezone(self.tz) + instance = instance.astimezone(tz) return cls( year=instance.year, month=instance.month, From 6fcb93fbfe171942bec5c0d26ebee8de461a2f6f Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Tue, 26 Mar 2024 19:28:34 +0200 Subject: [PATCH 11/19] Fixed linter and test issues (hopefully) --- piccolo/columns/column_types.py | 2 +- piccolo/columns/defaults/timestamptz.py | 22 ++++++++++++++-------- piccolo/table.py | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index ead6237f4..ee74c110a 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -33,7 +33,6 @@ class Band(Table): import uuid from dataclasses import dataclass from datetime import date, datetime, time, timedelta -from zoneinfo import ZoneInfo from enum import Enum from piccolo.columns.base import ( @@ -64,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 zoneinfo import ZoneInfo if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns.base import ColumnMeta diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index 0edbf3e80..0dd29d336 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -18,10 +18,12 @@ def __init__( tz: ZoneInfo = ZoneInfo('UTC') ): self.tz = tz - super().__init__(**{ - k: v for k, v in locals().items() - if k not in ['self', 'tz'] - }) + super().__init__( + days=days, + hours=hours, + minutes=minutes, + seconds=seconds + ) @property def cockroach(self): @@ -65,10 +67,14 @@ def __init__( tz: ZoneInfo = ZoneInfo('UTC') ): self.tz = tz - super().__init__(**{ - k: v for k, v in locals().items() - if k not in ['self', 'tz'] - }) + super().__init__( + year=year, + month=month, + day=day, + hour=hour, + second=second, + microsecond=microsecond + ) @property def cockroach(self): diff --git a/piccolo/table.py b/piccolo/table.py index 67d4eff26..5b66b934f 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -6,7 +6,6 @@ import typing as t import warnings from datetime import datetime -from zoneinfo import ZoneInfo from dataclasses import dataclass, field from piccolo.columns import Column @@ -57,6 +56,7 @@ from piccolo.utils.sql_values import convert_to_sql_value from piccolo.utils.sync import run_sync from piccolo.utils.warnings import colored_warning +from zoneinfo import ZoneInfo if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns import Selectable From 59a531e22349ba9742dad2df3a2fd1276e57a696 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Thu, 28 Mar 2024 12:26:49 +0200 Subject: [PATCH 12/19] Added backport.zoneinfo import with try-except clause, fixed imports ordering with isort --- piccolo/columns/column_types.py | 6 +++++- piccolo/columns/defaults/timestamptz.py | 6 +++++- piccolo/table.py | 8 ++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index ee74c110a..05353eab6 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -63,7 +63,11 @@ class Band(Table): from piccolo.querystring import QueryString, Unquoted from piccolo.utils.encoding import dump_json from piccolo.utils.warnings import colored_warning -from zoneinfo import ZoneInfo + +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns.base import ColumnMeta diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index 0dd29d336..c498594dd 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -3,7 +3,11 @@ import datetime import typing as t from enum import Enum -from zoneinfo import ZoneInfo + +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo from .timestamp import TimestampCustom, TimestampNow, TimestampOffset diff --git a/piccolo/table.py b/piccolo/table.py index 5b66b934f..ec76ee24f 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -5,8 +5,8 @@ import types import typing as t import warnings -from datetime import datetime from dataclasses import dataclass, field +from datetime import datetime from piccolo.columns import Column from piccolo.columns.column_types import ( @@ -56,7 +56,11 @@ from piccolo.utils.sql_values import convert_to_sql_value from piccolo.utils.sync import run_sync from piccolo.utils.warnings import colored_warning -from zoneinfo import ZoneInfo + +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns import Selectable From 7a3c58456b0f450c9e9467c38b508f15ea118958 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Thu, 28 Mar 2024 14:42:42 +0200 Subject: [PATCH 13/19] Fixed linting errors across codebase and fixed timestamptz test errors, migration tests are not fixed --- .../apps/migrations/auto/migration_manager.py | 18 +- piccolo/apps/migrations/auto/serialisation.py | 6 +- piccolo/apps/migrations/commands/backwards.py | 6 +- piccolo/apps/migrations/commands/base.py | 6 +- piccolo/apps/migrations/commands/forwards.py | 6 +- piccolo/apps/playground/commands/run.py | 1 + piccolo/apps/user/tables.py | 1 + piccolo/columns/base.py | 8 +- piccolo/columns/column_types.py | 186 ++++++------------ piccolo/columns/defaults/timestamptz.py | 45 ++--- piccolo/columns/reference.py | 1 + piccolo/conf/apps.py | 3 +- piccolo/query/methods/objects.py | 16 +- piccolo/query/methods/select.py | 12 +- piccolo/query/mixins.py | 6 +- piccolo/query/proxy.py | 5 +- piccolo/table.py | 16 +- tests/columns/test_reference.py | 1 + tests/columns/test_timestamptz.py | 106 +++++----- tests/conf/example.py | 1 + 20 files changed, 204 insertions(+), 246 deletions(-) diff --git a/piccolo/apps/migrations/auto/migration_manager.py b/piccolo/apps/migrations/auto/migration_manager.py index 772ec3edc..fca36e8e7 100644 --- a/piccolo/apps/migrations/auto/migration_manager.py +++ b/piccolo/apps/migrations/auto/migration_manager.py @@ -737,9 +737,9 @@ async def _run_rename_columns(self, backwards: bool = False): async def _run_add_tables(self, backwards: bool = False): table_classes: t.List[t.Type[Table]] = [] for add_table in self.add_tables: - add_columns: t.List[ - AddColumnClass - ] = self.add_columns.for_table_class_name(add_table.class_name) + add_columns: t.List[AddColumnClass] = ( + self.add_columns.for_table_class_name(add_table.class_name) + ) _Table: t.Type[Table] = create_table_class( class_name=add_table.class_name, class_kwargs={ @@ -792,9 +792,9 @@ async def _run_add_columns(self, backwards: bool = False): if table_class_name in [i.class_name for i in self.add_tables]: continue # No need to add columns to new tables - add_columns: t.List[ - AddColumnClass - ] = self.add_columns.for_table_class_name(table_class_name) + add_columns: t.List[AddColumnClass] = ( + self.add_columns.for_table_class_name(table_class_name) + ) ############################################################### # Define the table, with the columns, so the metaclass @@ -838,9 +838,9 @@ async def _run_add_columns(self, backwards: bool = False): else: primary_key = existing_table._meta.primary_key - table_class_members[ - primary_key._meta.name - ] = primary_key + table_class_members[primary_key._meta.name] = ( + primary_key + ) break diff --git a/piccolo/apps/migrations/auto/serialisation.py b/piccolo/apps/migrations/auto/serialisation.py index d1fd5ee47..b3644b853 100644 --- a/piccolo/apps/migrations/auto/serialisation.py +++ b/piccolo/apps/migrations/auto/serialisation.py @@ -25,8 +25,7 @@ class CanConflictWithGlobalNames(abc.ABC): @abc.abstractmethod - def warn_if_is_conflicting_with_global_name(self): - ... + def warn_if_is_conflicting_with_global_name(self): ... class UniqueGlobalNamesMeta(type): @@ -237,8 +236,7 @@ def warn_if_is_conflicting_with_global_name(self): class Definition(CanConflictWithGlobalNames, abc.ABC): @abc.abstractmethod - def __repr__(self): - ... + def __repr__(self): ... ########################################################################### # To allow sorting: diff --git a/piccolo/apps/migrations/commands/backwards.py b/piccolo/apps/migrations/commands/backwards.py index a0a454d90..c84e54556 100644 --- a/piccolo/apps/migrations/commands/backwards.py +++ b/piccolo/apps/migrations/commands/backwards.py @@ -31,9 +31,9 @@ def __init__( super().__init__() async def run_migrations_backwards(self, app_config: AppConfig): - migration_modules: t.Dict[ - str, MigrationModule - ] = self.get_migration_modules(app_config.migrations_folder_path) + migration_modules: t.Dict[str, MigrationModule] = ( + self.get_migration_modules(app_config.migrations_folder_path) + ) ran_migration_ids = await Migration.get_migrations_which_ran( app_name=self.app_name diff --git a/piccolo/apps/migrations/commands/base.py b/piccolo/apps/migrations/commands/base.py index 3b4cee100..a3966f7c3 100644 --- a/piccolo/apps/migrations/commands/base.py +++ b/piccolo/apps/migrations/commands/base.py @@ -88,9 +88,9 @@ async def get_migration_managers( migrations_folder = app_config.migrations_folder_path - migration_modules: t.Dict[ - str, MigrationModule - ] = self.get_migration_modules(migrations_folder) + migration_modules: t.Dict[str, MigrationModule] = ( + self.get_migration_modules(migrations_folder) + ) migration_ids = sorted(migration_modules.keys()) diff --git a/piccolo/apps/migrations/commands/forwards.py b/piccolo/apps/migrations/commands/forwards.py index f060b493a..6d967dd5e 100644 --- a/piccolo/apps/migrations/commands/forwards.py +++ b/piccolo/apps/migrations/commands/forwards.py @@ -32,9 +32,9 @@ async def run_migrations(self, app_config: AppConfig) -> MigrationResult: app_name=app_config.app_name ) - migration_modules: t.Dict[ - str, MigrationModule - ] = self.get_migration_modules(app_config.migrations_folder_path) + migration_modules: t.Dict[str, MigrationModule] = ( + self.get_migration_modules(app_config.migrations_folder_path) + ) ids = self.get_migration_ids(migration_modules) n = len(ids) diff --git a/piccolo/apps/playground/commands/run.py b/piccolo/apps/playground/commands/run.py index 32f840d51..b4bc23f70 100644 --- a/piccolo/apps/playground/commands/run.py +++ b/piccolo/apps/playground/commands/run.py @@ -2,6 +2,7 @@ Populates a database with an example schema and data, and launches a shell for interacting with the data using Piccolo. """ + import datetime import sys import typing as t diff --git a/piccolo/apps/user/tables.py b/piccolo/apps/user/tables.py index 878f06708..a9a389101 100644 --- a/piccolo/apps/user/tables.py +++ b/piccolo/apps/user/tables.py @@ -1,6 +1,7 @@ """ A User model, used for authentication. """ + from __future__ import annotations import datetime diff --git a/piccolo/columns/base.py b/piccolo/columns/base.py index d477dc992..886a0ee48 100644 --- a/piccolo/columns/base.py +++ b/piccolo/columns/base.py @@ -887,9 +887,11 @@ def get_sql_value(self, value: t.Any) -> t.Any: return ( "'{" + ", ".join( - f'"{i}"' - if isinstance(i, str) - else str(self.get_sql_value(i)) + ( + f'"{i}"' + if isinstance(i, str) + else str(self.get_sql_value(i)) + ) for i in value ) ) + "}'" diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index 05353eab6..68e6e0cec 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -67,7 +67,7 @@ class Band(Table): try: from zoneinfo import ZoneInfo except ImportError: - from backports.zoneinfo import ZoneInfo + from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns.base import ColumnMeta @@ -355,12 +355,10 @@ def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> str: - ... + def __get__(self, obj: Table, objtype=None) -> str: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Varchar: - ... + def __get__(self, obj: None, objtype=None) -> Varchar: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -394,12 +392,10 @@ def __init__(self, *args, **kwargs): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> str: - ... + def __get__(self, obj: Table, objtype=None) -> str: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Secret: - ... + def __get__(self, obj: None, objtype=None) -> Secret: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -461,12 +457,10 @@ def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> str: - ... + def __get__(self, obj: Table, objtype=None) -> str: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Text: - ... + def __get__(self, obj: None, objtype=None) -> Text: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -526,12 +520,10 @@ def __init__(self, default: UUIDArg = UUID4(), **kwargs) -> None: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> uuid.UUID: - ... + def __get__(self, obj: Table, objtype=None) -> uuid.UUID: ... @t.overload - def __get__(self, obj: None, objtype=None) -> UUID: - ... + def __get__(self, obj: None, objtype=None) -> UUID: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -646,12 +638,10 @@ def __rfloordiv__( # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> int: - ... + def __get__(self, obj: Table, objtype=None) -> int: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Integer: - ... + def __get__(self, obj: None, objtype=None) -> Integer: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -704,12 +694,10 @@ def column_type(self): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> int: - ... + def __get__(self, obj: Table, objtype=None) -> int: ... @t.overload - def __get__(self, obj: None, objtype=None) -> BigInt: - ... + def __get__(self, obj: None, objtype=None) -> BigInt: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -754,12 +742,10 @@ def column_type(self): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> int: - ... + def __get__(self, obj: Table, objtype=None) -> int: ... @t.overload - def __get__(self, obj: None, objtype=None) -> SmallInt: - ... + def __get__(self, obj: None, objtype=None) -> SmallInt: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -806,12 +792,10 @@ def default(self): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> int: - ... + def __get__(self, obj: Table, objtype=None) -> int: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Serial: - ... + def __get__(self, obj: None, objtype=None) -> Serial: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -840,12 +824,10 @@ def column_type(self): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> int: - ... + def __get__(self, obj: Table, objtype=None) -> int: ... @t.overload - def __get__(self, obj: None, objtype=None) -> BigSerial: - ... + def __get__(self, obj: None, objtype=None) -> BigSerial: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -875,12 +857,10 @@ def __init__(self, **kwargs) -> None: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> int: - ... + def __get__(self, obj: Table, objtype=None) -> int: ... @t.overload - def __get__(self, obj: None, objtype=None) -> PrimaryKey: - ... + def __get__(self, obj: None, objtype=None) -> PrimaryKey: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -965,12 +945,10 @@ def __sub__(self, value: timedelta) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> datetime: - ... + def __get__(self, obj: Table, objtype=None) -> datetime: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Timestamp: - ... + def __get__(self, obj: None, objtype=None) -> Timestamp: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -982,7 +960,7 @@ 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 when saved into the database and + 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** @@ -1025,9 +1003,9 @@ class TallinnConcerts(Table): def __init__( self, - tz: ZoneInfo = ZoneInfo('UTC'), - default: TimestamptzArg = TimestamptzNow(), - **kwargs + tz: ZoneInfo = ZoneInfo("UTC"), + default: TimestamptzArg = TimestamptzNow(), + **kwargs, ) -> None: self._validate_default( default, TimestamptzArg.__args__ # type: ignore @@ -1070,12 +1048,10 @@ def __sub__(self, value: timedelta) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> datetime: - ... + def __get__(self, obj: Table, objtype=None) -> datetime: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Timestamptz: - ... + def __get__(self, obj: None, objtype=None) -> Timestamptz: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1150,12 +1126,10 @@ def __sub__(self, value: timedelta) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> date: - ... + def __get__(self, obj: Table, objtype=None) -> date: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Date: - ... + def __get__(self, obj: None, objtype=None) -> Date: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1227,12 +1201,10 @@ def __sub__(self, value: timedelta) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> time: - ... + def __get__(self, obj: Table, objtype=None) -> time: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Time: - ... + def __get__(self, obj: None, objtype=None) -> Time: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1318,12 +1290,10 @@ def __sub__(self, value: timedelta) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> timedelta: - ... + def __get__(self, obj: Table, objtype=None) -> timedelta: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Interval: - ... + def __get__(self, obj: None, objtype=None) -> Interval: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1412,12 +1382,10 @@ def ne(self, value) -> Where: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> bool: - ... + def __get__(self, obj: Table, objtype=None) -> bool: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Boolean: - ... + def __get__(self, obj: None, objtype=None) -> Boolean: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1515,12 +1483,10 @@ def __init__( # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> decimal.Decimal: - ... + def __get__(self, obj: Table, objtype=None) -> decimal.Decimal: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Numeric: - ... + def __get__(self, obj: None, objtype=None) -> Numeric: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1538,12 +1504,10 @@ class Decimal(Numeric): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> decimal.Decimal: - ... + def __get__(self, obj: Table, objtype=None) -> decimal.Decimal: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Decimal: - ... + def __get__(self, obj: None, objtype=None) -> Decimal: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1589,12 +1553,10 @@ def __init__( # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> float: - ... + def __get__(self, obj: Table, objtype=None) -> float: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Real: - ... + def __get__(self, obj: None, objtype=None) -> Real: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1612,12 +1574,10 @@ class Float(Real): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> float: - ... + def __get__(self, obj: Table, objtype=None) -> float: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Float: - ... + def __get__(self, obj: None, objtype=None) -> Float: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1639,12 +1599,10 @@ def column_type(self): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> float: - ... + def __get__(self, obj: Table, objtype=None) -> float: ... @t.overload - def __get__(self, obj: None, objtype=None) -> DoublePrecision: - ... + def __get__(self, obj: None, objtype=None) -> DoublePrecision: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -1884,8 +1842,7 @@ def __init__( on_update: OnUpdate = OnUpdate.cascade, target_column: t.Union[str, Column, None] = None, **kwargs, - ) -> None: - ... + ) -> None: ... @t.overload def __init__( @@ -1897,8 +1854,7 @@ def __init__( on_update: OnUpdate = OnUpdate.cascade, target_column: t.Union[str, Column, None] = None, **kwargs, - ) -> None: - ... + ) -> None: ... @t.overload def __init__( @@ -1910,8 +1866,7 @@ def __init__( on_update: OnUpdate = OnUpdate.cascade, target_column: t.Union[str, Column, None] = None, **kwargs, - ) -> None: - ... + ) -> None: ... def __init__( self, @@ -2261,16 +2216,15 @@ def __getattribute__(self, name: str) -> t.Union[Column, t.Any]: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> t.Any: - ... + def __get__(self, obj: Table, objtype=None) -> t.Any: ... @t.overload - def __get__(self, obj: None, objtype=None) -> ForeignKey[ReferencedTable]: - ... + def __get__( + self, obj: None, objtype=None + ) -> ForeignKey[ReferencedTable]: ... @t.overload - def __get__(self, obj: t.Any, objtype=None) -> t.Any: - ... + def __get__(self, obj: t.Any, objtype=None) -> t.Any: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -2330,12 +2284,10 @@ def column_type(self): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> str: - ... + def __get__(self, obj: Table, objtype=None) -> str: ... @t.overload - def __get__(self, obj: None, objtype=None) -> JSON: - ... + def __get__(self, obj: None, objtype=None) -> JSON: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -2400,12 +2352,10 @@ def ne(self, value) -> Where: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> str: - ... + def __get__(self, obj: Table, objtype=None) -> str: ... @t.overload - def __get__(self, obj: None, objtype=None) -> JSONB: - ... + def __get__(self, obj: None, objtype=None) -> JSONB: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -2473,12 +2423,10 @@ def __init__( # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> bytes: - ... + def __get__(self, obj: Table, objtype=None) -> bytes: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Bytea: - ... + def __get__(self, obj: None, objtype=None) -> Bytea: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -2496,12 +2444,10 @@ class Blob(Bytea): # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> bytes: - ... + def __get__(self, obj: Table, objtype=None) -> bytes: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Blob: - ... + def __get__(self, obj: None, objtype=None) -> Blob: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self @@ -2752,12 +2698,10 @@ def __add__(self, value: t.List[t.Any]) -> QueryString: # Descriptors @t.overload - def __get__(self, obj: Table, objtype=None) -> t.List[t.Any]: - ... + def __get__(self, obj: Table, objtype=None) -> t.List[t.Any]: ... @t.overload - def __get__(self, obj: None, objtype=None) -> Array: - ... + def __get__(self, obj: None, objtype=None) -> Array: ... def __get__(self, obj, objtype=None): return obj.__dict__[self._meta.name] if obj else self diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index c498594dd..a055c032b 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -1,34 +1,31 @@ from __future__ import annotations -import datetime +import datetime as pydatetime import typing as t from enum import Enum try: from zoneinfo import ZoneInfo except ImportError: - from backports.zoneinfo import ZoneInfo + from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 from .timestamp import TimestampCustom, TimestampNow, TimestampOffset class TimestamptzOffset(TimestampOffset): def __init__( - self, - days: int = 0, - hours: int = 0, - minutes: int = 0, + self, + days: int = 0, + hours: int = 0, + minutes: int = 0, seconds: int = 0, - tz: ZoneInfo = ZoneInfo('UTC') + tz: ZoneInfo = ZoneInfo("UTC"), ): self.tz = tz super().__init__( - days=days, - hours=hours, - minutes=minutes, - seconds=seconds + days=days, hours=hours, minutes=minutes, seconds=seconds ) - + @property def cockroach(self): interval_string = self.get_postgres_interval_string( @@ -37,9 +34,7 @@ def cockroach(self): return f"CURRENT_TIMESTAMP + INTERVAL '{interval_string}'" def python(self): - return datetime.datetime.now( - tz=self.tz - ) + datetime.timedelta( + return pydatetime.datetime.now(tz=self.tz) + pydatetime.timedelta( days=self.days, hours=self.hours, minutes=self.minutes, @@ -48,15 +43,15 @@ def python(self): class TimestamptzNow(TimestampNow): - def __init__(self, tz: ZoneInfo = ZoneInfo('UTC')): + def __init__(self, tz: ZoneInfo = ZoneInfo("UTC")): self.tz = tz - + @property def cockroach(self): return "current_timestamp" def python(self): - return datetime.datetime.now(tz=self.tz) + return pydatetime.datetime.now(tz=self.tz) class TimestamptzCustom(TimestampCustom): @@ -68,7 +63,7 @@ def __init__( hour: int = 0, second: int = 0, microsecond: int = 0, - tz: ZoneInfo = ZoneInfo('UTC') + tz: ZoneInfo = ZoneInfo("UTC"), ): self.tz = tz super().__init__( @@ -77,16 +72,16 @@ def __init__( day=day, hour=hour, second=second, - microsecond=microsecond + microsecond=microsecond, ) - + @property def cockroach(self): return "'{}'".format(self.datetime.isoformat().replace("T", " ")) @property def datetime(self): - return datetime.datetime( + return pydatetime.datetime( year=self.year, month=self.month, day=self.day, @@ -97,7 +92,9 @@ def datetime(self): ) @classmethod - def from_datetime(cls, instance: datetime.datetime, tz: ZoneInfo = ZoneInfo('UTC')): # type: ignore + def from_datetime( + cls, instance: pydatetime.datetime, tz: ZoneInfo = ZoneInfo("UTC") + ): # type: ignore if instance.tzinfo is not None: instance = instance.astimezone(tz) return cls( @@ -116,7 +113,7 @@ def from_datetime(cls, instance: datetime.datetime, tz: ZoneInfo = ZoneInfo('UTC TimestamptzOffset, Enum, None, - datetime.datetime, + pydatetime.datetime, ] diff --git a/piccolo/columns/reference.py b/piccolo/columns/reference.py index 841545eeb..f6edcdd56 100644 --- a/piccolo/columns/reference.py +++ b/piccolo/columns/reference.py @@ -1,6 +1,7 @@ """ Dataclasses for storing lazy references between ForeignKey columns and tables. """ + from __future__ import annotations import importlib diff --git a/piccolo/conf/apps.py b/piccolo/conf/apps.py index c311e1569..47631c478 100644 --- a/piccolo/conf/apps.py +++ b/piccolo/conf/apps.py @@ -25,8 +25,7 @@ class MigrationModule(ModuleType): @staticmethod @abstractmethod - async def forwards() -> MigrationManager: - ... + async def forwards() -> MigrationManager: ... class PiccoloAppModule(ModuleType): diff --git a/piccolo/query/methods/objects.py b/piccolo/query/methods/objects.py index 5b1c96002..7b8c3ad43 100644 --- a/piccolo/query/methods/objects.py +++ b/piccolo/query/methods/objects.py @@ -124,10 +124,10 @@ async def run( results = objects[0] if objects else None - modified_response: t.Optional[ - TableInstance - ] = await self.query.callback_delegate.invoke( - results=results, kind=CallbackType.success + modified_response: t.Optional[TableInstance] = ( + await self.query.callback_delegate.invoke( + results=results, kind=CallbackType.success + ) ) return modified_response @@ -355,10 +355,10 @@ async def run( # With callbacks, the user can return any data that they want. # Assume that most of the time they will still return a list of # Table instances. - modified: t.List[ - TableInstance - ] = await self.callback_delegate.invoke( - results, kind=CallbackType.success + modified: t.List[TableInstance] = ( + await self.callback_delegate.invoke( + results, kind=CallbackType.success + ) ) return modified else: diff --git a/piccolo/query/methods/select.py b/piccolo/query/methods/select.py index a00745e48..a2a77b155 100644 --- a/piccolo/query/methods/select.py +++ b/piccolo/query/methods/select.py @@ -604,20 +604,16 @@ def order_by( return self @t.overload - def output(self: Self, *, as_list: bool) -> SelectList: - ... + def output(self: Self, *, as_list: bool) -> SelectList: ... @t.overload - def output(self: Self, *, as_json: bool) -> SelectJSON: - ... + def output(self: Self, *, as_json: bool) -> SelectJSON: ... @t.overload - def output(self: Self, *, load_json: bool) -> Self: - ... + def output(self: Self, *, load_json: bool) -> Self: ... @t.overload - def output(self: Self, *, nested: bool) -> Self: - ... + def output(self: Self, *, nested: bool) -> Self: ... def output( self: Self, diff --git a/piccolo/query/mixins.py b/piccolo/query/mixins.py index 56be35a8f..8d7c6a4a9 100644 --- a/piccolo/query/mixins.py +++ b/piccolo/query/mixins.py @@ -639,9 +639,9 @@ class OnConflictAction(str, Enum): class OnConflictItem: target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None action: t.Optional[OnConflictAction] = None - values: t.Optional[ - t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]] - ] = None + values: t.Optional[t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]] = ( + None + ) where: t.Optional[Combinable] = None @property diff --git a/piccolo/query/proxy.py b/piccolo/query/proxy.py index 7ded47b84..30ce06083 100644 --- a/piccolo/query/proxy.py +++ b/piccolo/query/proxy.py @@ -8,8 +8,9 @@ class Runnable(Protocol): - async def run(self, node: t.Optional[str] = None, in_pool: bool = True): - ... + async def run( + self, node: t.Optional[str] = None, in_pool: bool = True + ): ... QueryType = t.TypeVar("QueryType", bound=Runnable) diff --git a/piccolo/table.py b/piccolo/table.py index ec76ee24f..3772b9ab3 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -60,7 +60,7 @@ try: from zoneinfo import ZoneInfo except ImportError: - from backports.zoneinfo import ZoneInfo + from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns import Selectable @@ -445,7 +445,7 @@ def __init__( if isinstance(column, Timestamptz) and isinstance(value, datetime): value = value.astimezone(column.tz) - + self[column._meta.name] = value unrecognized = kwargs.keys() @@ -576,12 +576,10 @@ def refresh( @t.overload def get_related( self, foreign_key: ForeignKey[ReferencedTable] - ) -> First[ReferencedTable]: - ... + ) -> First[ReferencedTable]: ... @t.overload - def get_related(self, foreign_key: str) -> First[Table]: - ... + def get_related(self, foreign_key: str) -> First[Table]: ... def get_related( self, foreign_key: t.Union[str, ForeignKey[ReferencedTable]] @@ -755,9 +753,9 @@ def to_dict(self, *columns: Column) -> t.Dict[str, t.Any]: if isinstance(value, Table): value = value.to_dict(*columns) - output[ - alias_names.get(column._meta.name) or column._meta.name - ] = value + output[alias_names.get(column._meta.name) or column._meta.name] = ( + value + ) return output def __setitem__(self, key: str, value: t.Any): diff --git a/tests/columns/test_reference.py b/tests/columns/test_reference.py index ea9887e00..21daa2f58 100644 --- a/tests/columns/test_reference.py +++ b/tests/columns/test_reference.py @@ -2,6 +2,7 @@ Most of the tests for piccolo/columns/reference.py are covered in piccolo/columns/test_foreignkey.py """ + from unittest import TestCase from piccolo.columns.reference import LazyTableReference diff --git a/tests/columns/test_timestamptz.py b/tests/columns/test_timestamptz.py index 8e239900b..ee690938e 100644 --- a/tests/columns/test_timestamptz.py +++ b/tests/columns/test_timestamptz.py @@ -1,8 +1,7 @@ import datetime +from operator import eq from unittest import TestCase -from dateutil import tz - from piccolo.columns.column_types import Timestamptz from piccolo.columns.defaults.timestamptz import ( TimestamptzCustom, @@ -11,9 +10,19 @@ ) from piccolo.table import Table +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 + + +UTC_TZ = ZoneInfo("UTC") +LOCAL_TZ = ZoneInfo("Europe/Tallinn") + class MyTable(Table): - created_on = Timestamptz() + created_on_utc = Timestamptz(tz=UTC_TZ) + created_on_local = Timestamptz(tz=LOCAL_TZ) class MyTableDefault(Table): @@ -22,18 +31,19 @@ class MyTableDefault(Table): `Timestamptz`. """ - created_on = Timestamptz(default=TimestamptzNow()) - created_on_offset = Timestamptz(default=TimestamptzOffset(days=1)) - created_on_custom = Timestamptz(default=TimestamptzCustom(year=2021)) + created_on = Timestamptz(default=TimestamptzNow(tz=LOCAL_TZ), tz=LOCAL_TZ) + created_on_offset = Timestamptz( + default=TimestamptzOffset(days=1, tz=LOCAL_TZ), tz=LOCAL_TZ + ) + created_on_custom = Timestamptz( + default=TimestamptzCustom(year=2021, tz=LOCAL_TZ), tz=LOCAL_TZ + ) created_on_datetime = Timestamptz( - default=datetime.datetime(year=2020, month=1, day=1) + default=datetime.datetime(year=2020, month=1, day=1, tzinfo=LOCAL_TZ), + tz=LOCAL_TZ, ) -class CustomTimezone(datetime.tzinfo): - pass - - class TestTimestamptz(TestCase): def setUp(self): MyTable.create_table().run_sync() @@ -45,37 +55,32 @@ def test_timestamptz_timezone_aware(self): """ Test storing a timezone aware timestamp. """ - for tzinfo in ( - datetime.timezone.utc, - tz.gettz("America/New_York"), - ): - created_on = datetime.datetime( - year=2020, - month=1, - day=1, - hour=12, - minute=0, - second=0, - tzinfo=tzinfo, - ) - row = MyTable(created_on=created_on) - row.save().run_sync() - - # Fetch it back from the database - result = ( - MyTable.objects() - .where( - MyTable._meta.primary_key - == getattr(row, MyTable._meta.primary_key._meta.name) - ) - .first() - .run_sync() - ) - assert result is not None - self.assertEqual(result.created_on, created_on) - - # The database converts it to UTC - self.assertEqual(result.created_on.tzinfo, datetime.timezone.utc) + dt_args = dict(year=2020, month=1, day=1, hour=12, minute=0, second=0) + created_on_utc = datetime.datetime(**dt_args, tzinfo=ZoneInfo("UTC")) + created_on_local = datetime.datetime( + **dt_args, tzinfo=ZoneInfo("Europe/Tallinn") + ) + row = MyTable( + created_on_utc=created_on_utc, created_on_local=created_on_local + ) + row.save().run_sync() + + # Fetch it back from the database + p_key = MyTable._meta.primary_key + p_key_name = getattr(row, p_key._meta.name) + result = ( + MyTable.objects().where(eq(p_key, p_key_name)).first().run_sync() + ) + assert result is not None + self.assertEqual(result.created_on_utc, created_on_utc) + self.assertEqual(result.created_on_local, created_on_local) + + # The database stores the datetime of the column in UTC timezone, but + # the column converts it back to the timezone that is defined for it. + self.assertEqual(result.created_on_utc.tzinfo, created_on_utc.tzinfo) + self.assertEqual( + result.created_on_local.tzinfo, created_on_local.tzinfo + ) class TestTimestamptzDefault(TestCase): @@ -89,12 +94,25 @@ def test_timestamptz_default(self): """ Make sure the default value gets created, and can be retrieved. """ - created_on = datetime.datetime.now(tz=datetime.timezone.utc) + created_on = datetime.datetime.now(tz=LOCAL_TZ) row = MyTableDefault() row.save().run_sync() result = MyTableDefault.objects().first().run_sync() assert result is not None + delta = result.created_on - created_on self.assertLess(delta, datetime.timedelta(seconds=1)) - self.assertEqual(result.created_on.tzinfo, datetime.timezone.utc) + self.assertEqual(result.created_on.tzinfo, created_on.tzinfo) + + delta = result.created_on_offset - created_on + self.assertLessEqual(delta, datetime.timedelta(days=1)) + self.assertEqual(result.created_on_offset.tzinfo, created_on.tzinfo) + + delta = created_on - result.created_on_custom + self.assertGreaterEqual(delta, datetime.timedelta(days=delta.days)) + self.assertEqual(result.created_on_custom.tzinfo, created_on.tzinfo) + + delta = created_on - result.created_on_datetime + self.assertGreaterEqual(delta, datetime.timedelta(days=delta.days)) + self.assertEqual(result.created_on_datetime.tzinfo, created_on.tzinfo) diff --git a/tests/conf/example.py b/tests/conf/example.py index c7dce05a5..ef4541627 100644 --- a/tests/conf/example.py +++ b/tests/conf/example.py @@ -2,6 +2,7 @@ This file is used by test_apps.py to make sure we can exclude imported ``Table`` subclasses when using ``table_finder``. """ + from piccolo.apps.user.tables import BaseUser from piccolo.columns.column_types import ForeignKey, Varchar from piccolo.table import Table From 41a6d0c59ea3a4936c0e6ae14e81acc3ad03abd8 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Thu, 28 Mar 2024 17:47:01 +0200 Subject: [PATCH 14/19] Fixed zoneinfo module import for autogenerated migrations files --- piccolo/apps/migrations/auto/serialisation.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/piccolo/apps/migrations/auto/serialisation.py b/piccolo/apps/migrations/auto/serialisation.py index b3644b853..9791d2f9f 100644 --- a/piccolo/apps/migrations/auto/serialisation.py +++ b/piccolo/apps/migrations/auto/serialisation.py @@ -12,12 +12,22 @@ from dataclasses import dataclass, field from enum import Enum -from piccolo.columns import Column +from piccolo.columns import Column, Timestamptz from piccolo.columns.defaults.base import Default +from piccolo.columns.defaults.timestamptz import ( + TimestamptzCustom, + TimestamptzNow, + TimestamptzOffset, +) from piccolo.columns.reference import LazyTableReference from piccolo.table import Table from piccolo.utils.repr import repr_class_instance +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 + from .serialisation_legacy import deserialise_legacy_params ############################################################################### @@ -546,8 +556,29 @@ def serialise_params(params: t.Dict[str, t.Any]) -> SerialisedParams: expect_conflict_with_global_name=UniqueGlobalNames.DEFAULT, ) ) + # ZoneInfo for Timestamptz* instances + in_group = ( + Timestamptz, TimestamptzNow, + TimestamptzCustom, TimestamptzOffset + ) + if isinstance(value, in_group): + extra_imports.append( + Import( + module=ZoneInfo.__module__, + target=None, + ) + ) continue + # ZoneInfo instances + if isinstance(value, ZoneInfo): + extra_imports.append( + Import( + module=value.__class__.__module__, + target=None, + ) + ) + # Dates and times if isinstance( value, (datetime.time, datetime.datetime, datetime.date) From ad0d1d15d9030e8b034639fa156f3373c1c3e74c Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Thu, 28 Mar 2024 20:53:01 +0200 Subject: [PATCH 15/19] I swear to god if this commit doesn't fix the linting and test errors --- piccolo/apps/migrations/auto/serialisation.py | 6 ++++-- tests/columns/test_timestamptz.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/piccolo/apps/migrations/auto/serialisation.py b/piccolo/apps/migrations/auto/serialisation.py index 9791d2f9f..a06cf6bfe 100644 --- a/piccolo/apps/migrations/auto/serialisation.py +++ b/piccolo/apps/migrations/auto/serialisation.py @@ -558,8 +558,10 @@ def serialise_params(params: t.Dict[str, t.Any]) -> SerialisedParams: ) # ZoneInfo for Timestamptz* instances in_group = ( - Timestamptz, TimestamptzNow, - TimestamptzCustom, TimestamptzOffset + Timestamptz, + TimestamptzNow, + TimestamptzCustom, + TimestamptzOffset, ) if isinstance(value, in_group): extra_imports.append( diff --git a/tests/columns/test_timestamptz.py b/tests/columns/test_timestamptz.py index ee690938e..3e36b0cf6 100644 --- a/tests/columns/test_timestamptz.py +++ b/tests/columns/test_timestamptz.py @@ -1,4 +1,5 @@ import datetime +import time from operator import eq from unittest import TestCase @@ -95,6 +96,8 @@ def test_timestamptz_default(self): Make sure the default value gets created, and can be retrieved. """ created_on = datetime.datetime.now(tz=LOCAL_TZ) + time.sleep(1e-5) + row = MyTableDefault() row.save().run_sync() @@ -106,7 +109,7 @@ def test_timestamptz_default(self): self.assertEqual(result.created_on.tzinfo, created_on.tzinfo) delta = result.created_on_offset - created_on - self.assertLessEqual(delta, datetime.timedelta(days=1)) + self.assertGreaterEqual(delta, datetime.timedelta(days=1)) self.assertEqual(result.created_on_offset.tzinfo, created_on.tzinfo) delta = created_on - result.created_on_custom From c816d8ec62b63ac388d8b5490b1843288d5dc6df Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Thu, 28 Mar 2024 21:56:23 +0200 Subject: [PATCH 16/19] Added more ZoneInfo import ignore rules for MyPy --- piccolo/apps/migrations/auto/serialisation.py | 2 +- piccolo/columns/column_types.py | 2 +- piccolo/columns/defaults/timestamptz.py | 2 +- piccolo/table.py | 2 +- tests/columns/test_timestamptz.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/piccolo/apps/migrations/auto/serialisation.py b/piccolo/apps/migrations/auto/serialisation.py index a06cf6bfe..81ee23801 100644 --- a/piccolo/apps/migrations/auto/serialisation.py +++ b/piccolo/apps/migrations/auto/serialisation.py @@ -24,7 +24,7 @@ from piccolo.utils.repr import repr_class_instance try: - from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfo # type: ignore except ImportError: from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index 68e6e0cec..428cb3c4c 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -65,7 +65,7 @@ class Band(Table): from piccolo.utils.warnings import colored_warning try: - from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfo # type: ignore except ImportError: from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index a055c032b..ffb04ec55 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -5,7 +5,7 @@ from enum import Enum try: - from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfo # type: ignore except ImportError: from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 diff --git a/piccolo/table.py b/piccolo/table.py index 3772b9ab3..00d5c9502 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -58,7 +58,7 @@ from piccolo.utils.warnings import colored_warning try: - from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfo # type: ignore except ImportError: from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 diff --git a/tests/columns/test_timestamptz.py b/tests/columns/test_timestamptz.py index 3e36b0cf6..163919e78 100644 --- a/tests/columns/test_timestamptz.py +++ b/tests/columns/test_timestamptz.py @@ -12,7 +12,7 @@ from piccolo.table import Table try: - from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfo # type: ignore except ImportError: from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 From cbf6d41b64a36b3c6c7e60430c2130ef88baac93 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Fri, 29 Mar 2024 00:04:26 +0200 Subject: [PATCH 17/19] Added pragma no cover to ZoneInfo import except clauses --- piccolo/apps/migrations/auto/serialisation.py | 2 +- piccolo/columns/column_types.py | 2 +- piccolo/columns/defaults/timestamptz.py | 2 +- piccolo/table.py | 2 +- tests/columns/test_timestamptz.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/piccolo/apps/migrations/auto/serialisation.py b/piccolo/apps/migrations/auto/serialisation.py index 81ee23801..83ed4d221 100644 --- a/piccolo/apps/migrations/auto/serialisation.py +++ b/piccolo/apps/migrations/auto/serialisation.py @@ -25,7 +25,7 @@ try: from zoneinfo import ZoneInfo # type: ignore -except ImportError: +except ImportError: # pragma: no cover from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 from .serialisation_legacy import deserialise_legacy_params diff --git a/piccolo/columns/column_types.py b/piccolo/columns/column_types.py index 428cb3c4c..34ebb3f64 100644 --- a/piccolo/columns/column_types.py +++ b/piccolo/columns/column_types.py @@ -66,7 +66,7 @@ class Band(Table): try: from zoneinfo import ZoneInfo # type: ignore -except ImportError: +except ImportError: # pragma: no cover from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 if t.TYPE_CHECKING: # pragma: no cover diff --git a/piccolo/columns/defaults/timestamptz.py b/piccolo/columns/defaults/timestamptz.py index ffb04ec55..90ba0fa9f 100644 --- a/piccolo/columns/defaults/timestamptz.py +++ b/piccolo/columns/defaults/timestamptz.py @@ -6,7 +6,7 @@ try: from zoneinfo import ZoneInfo # type: ignore -except ImportError: +except ImportError: # pragma: no cover from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 from .timestamp import TimestampCustom, TimestampNow, TimestampOffset diff --git a/piccolo/table.py b/piccolo/table.py index 00d5c9502..c8e5fd479 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -59,7 +59,7 @@ try: from zoneinfo import ZoneInfo # type: ignore -except ImportError: +except ImportError: # pragma: no cover from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 if t.TYPE_CHECKING: # pragma: no cover diff --git a/tests/columns/test_timestamptz.py b/tests/columns/test_timestamptz.py index 163919e78..7d8ee3da3 100644 --- a/tests/columns/test_timestamptz.py +++ b/tests/columns/test_timestamptz.py @@ -13,7 +13,7 @@ try: from zoneinfo import ZoneInfo # type: ignore -except ImportError: +except ImportError: # pragma: no cover from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 From db539394018bfdb1e03b2c67e295cabe61a03cf6 Mon Sep 17 00:00:00 2001 From: Mattias Aabmets Date: Fri, 29 Mar 2024 10:14:05 +0200 Subject: [PATCH 18/19] Removed ZoneInfo import from table.py --- piccolo/table.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/piccolo/table.py b/piccolo/table.py index c8e5fd479..2dc3e1cae 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -57,11 +57,6 @@ from piccolo.utils.sync import run_sync from piccolo.utils.warnings import colored_warning -try: - from zoneinfo import ZoneInfo # type: ignore -except ImportError: # pragma: no cover - from backports.zoneinfo import ZoneInfo # type: ignore # noqa: F401 - if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns import Selectable From 64cb95e962b5dd898e8d61fb1e12316105a1979e Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Fri, 5 Apr 2024 14:03:07 +0100 Subject: [PATCH 19/19] add missing `continue` --- piccolo/apps/migrations/auto/serialisation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/piccolo/apps/migrations/auto/serialisation.py b/piccolo/apps/migrations/auto/serialisation.py index 83ed4d221..872864080 100644 --- a/piccolo/apps/migrations/auto/serialisation.py +++ b/piccolo/apps/migrations/auto/serialisation.py @@ -580,6 +580,7 @@ def serialise_params(params: t.Dict[str, t.Any]) -> SerialisedParams: target=None, ) ) + continue # Dates and times if isinstance( @@ -666,6 +667,7 @@ def serialise_params(params: t.Dict[str, t.Any]) -> SerialisedParams: extra_imports.append( Import(module=module_name, target=type_.__name__) ) + continue # Functions if inspect.isfunction(value):