Compare commits

...

1 Commits

Author SHA1 Message Date
Evan Rusackas
8077c7b076 chore: replace deprecated datetime.utcnow()/utcfromtimestamp()
Python 3.12 deprecated datetime.utcnow() and datetime.utcfromtimestamp().
This sweeps all production occurrences under superset/ (8 files) and replaces
them with behavior-preserving equivalents that keep the existing naive-UTC
semantics:

  datetime.utcnow()            -> datetime.now(timezone.utc).replace(tzinfo=None)
  datetime.utcfromtimestamp(x) -> datetime.fromtimestamp(x, timezone.utc).replace(tzinfo=None)

Keeping the values naive (rather than switching to aware datetimes) avoids
naive/aware comparison errors against timestamps already stored naive in the
metadata DB (e.g. the Log.dttm prune query on PostgreSQL) and keeps
isoformat()/cache-key output byte-identical. This is the key difference from
the earlier #37538, which switched to aware datetimes.

superset/security/session_invalidation.py already uses now(timezone.utc) and
is left unchanged. Test files still use the deprecated calls and can be
migrated in a focused follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 00:53:59 -07:00
8 changed files with 68 additions and 37 deletions

View File

@@ -16,7 +16,7 @@
# under the License.
import logging
import time
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import sqlalchemy as sa
@@ -65,10 +65,12 @@ class LogPruneCommand(BaseCommand):
# Select all IDs that need to be deleted
# Log.dttm is stored as a naive UTC datetime (no tzinfo), so compute
# the cutoff with utcnow() to avoid a naive/aware mismatch that raises
# the cutoff as a naive UTC datetime to avoid a naive/aware mismatch that raises
# on PostgreSQL ("operator does not exist: timestamp without time zone").
select_stmt = sa.select(Log.id).where(
Log.dttm < datetime.utcnow() - timedelta(days=self.retention_period_days)
Log.dttm
< datetime.now(timezone.utc).replace(tzinfo=None)
- timedelta(days=self.retention_period_days)
)
# Optionally limited by max_rows_per_run

View File

@@ -16,7 +16,7 @@
# under the License.
import logging
from collections.abc import Sequence
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any, Optional, TYPE_CHECKING, Union
from uuid import UUID
@@ -127,7 +127,7 @@ class BaseReportState:
) -> None:
self._report_schedule = report_schedule
self._scheduled_dttm = scheduled_dttm
self._start_dttm = datetime.utcnow()
self._start_dttm = datetime.now(timezone.utc).replace(tzinfo=None)
self._execution_id = execution_id
self._filter_warnings: list[str] = []
@@ -157,7 +157,9 @@ class BaseReportState:
self._report_schedule.last_value_row_json = None
self._report_schedule.last_state = state
self._report_schedule.last_eval_dttm = datetime.utcnow()
self._report_schedule.last_eval_dttm = datetime.now(timezone.utc).replace(
tzinfo=None
)
def update_report_schedule_slack_v2(self) -> None:
"""
@@ -229,7 +231,7 @@ class BaseReportState:
log = ReportExecutionLog(
scheduled_dttm=self._scheduled_dttm,
start_dttm=self._start_dttm,
end_dttm=datetime.utcnow(),
end_dttm=datetime.now(timezone.utc).replace(tzinfo=None),
value=self._report_schedule.last_value,
value_row_json=self._report_schedule.last_value_row_json,
state=self._report_schedule.last_state,
@@ -464,7 +466,7 @@ class BaseReportState:
Get chart or dashboard screenshots
:raises: ReportScheduleScreenshotFailedError
"""
start_time = datetime.utcnow()
start_time = datetime.now(timezone.utc).replace(tzinfo=None)
user, _ = resolve_executor_user(self._report_schedule)
@@ -511,14 +513,18 @@ class BaseReportState:
"Screenshot failed; aborting to avoid sending a partial report"
)
imges.append(imge)
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.info(
"Screenshot capture took %.2fs - execution_id: %s",
elapsed_seconds,
self._execution_id,
)
except SoftTimeLimitExceeded as ex:
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.warning(
"Screenshot timeout after %.2fs - execution_id: %s",
elapsed_seconds,
@@ -526,7 +532,9 @@ class BaseReportState:
)
raise ReportScheduleScreenshotTimeout() from ex
except Exception as ex:
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.error(
"Screenshot failed after %.2fs - execution_id: %s",
elapsed_seconds,
@@ -550,7 +558,7 @@ class BaseReportState:
return pdf
def _get_csv_data(self) -> bytes:
start_time = datetime.utcnow()
start_time = datetime.now(timezone.utc).replace(tzinfo=None)
url = self._get_url(result_format=ChartDataResultFormat.CSV)
user, username = resolve_executor_user(self._report_schedule)
auth_cookies = machine_auth_provider_factory.instance.get_auth_cookies(user)
@@ -565,7 +573,9 @@ class BaseReportState:
auth_cookies=auth_cookies,
timeout=app.config["ALERT_REPORTS_CSV_REQUEST_TIMEOUT"],
)
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.info(
"CSV data generation from %s as user %s took %.2fs - execution_id: %s",
url,
@@ -574,7 +584,9 @@ class BaseReportState:
self._execution_id,
)
except SoftTimeLimitExceeded as ex:
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.warning(
"CSV generation timeout after %.2fs - execution_id: %s",
elapsed_seconds,
@@ -582,7 +594,9 @@ class BaseReportState:
)
raise ReportScheduleCsvTimeout() from ex
except Exception as ex:
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.exception(
"CSV generation failed after %.2fs - execution_id: %s",
elapsed_seconds,
@@ -599,7 +613,7 @@ class BaseReportState:
"""
Return data as a Pandas dataframe, to embed in notifications as a table.
"""
start_time = datetime.utcnow()
start_time = datetime.now(timezone.utc).replace(tzinfo=None)
url = self._get_url(result_format=ChartDataResultFormat.JSON)
user, username = resolve_executor_user(self._report_schedule)
@@ -615,7 +629,9 @@ class BaseReportState:
auth_cookies,
timeout=app.config["ALERT_REPORTS_CSV_REQUEST_TIMEOUT"],
)
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.info(
"DataFrame generation from %s as user %s took %.2fs - execution_id: %s",
url,
@@ -624,7 +640,9 @@ class BaseReportState:
self._execution_id,
)
except SoftTimeLimitExceeded as ex:
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.warning(
"DataFrame generation timeout after %.2fs - execution_id: %s",
elapsed_seconds,
@@ -632,7 +650,9 @@ class BaseReportState:
)
raise ReportScheduleDataFrameTimeout() from ex
except Exception as ex:
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.error(
"DataFrame generation failed after %.2fs - execution_id: %s",
elapsed_seconds,
@@ -875,7 +895,7 @@ class BaseReportState:
return (
last_success is not None
and self._report_schedule.grace_period
and datetime.utcnow()
and datetime.now(timezone.utc).replace(tzinfo=None)
- timedelta(seconds=self._report_schedule.grace_period)
< last_success.end_dttm
)
@@ -892,7 +912,7 @@ class BaseReportState:
return (
last_success is not None
and self._report_schedule.grace_period
and datetime.utcnow()
and datetime.now(timezone.utc).replace(tzinfo=None)
- timedelta(seconds=self._report_schedule.grace_period)
< last_success.end_dttm
)
@@ -909,7 +929,7 @@ class BaseReportState:
return (
self._report_schedule.working_timeout is not None
and self._report_schedule.last_eval_dttm is not None
and datetime.utcnow()
and datetime.now(timezone.utc).replace(tzinfo=None)
- timedelta(seconds=self._report_schedule.working_timeout)
> last_working.end_dttm
)
@@ -1025,7 +1045,10 @@ class ReportWorkingState(BaseReportState):
self._report_schedule
)
elapsed_seconds = (
(datetime.utcnow() - last_working.end_dttm).total_seconds()
(
datetime.now(timezone.utc).replace(tzinfo=None)
- last_working.end_dttm
).total_seconds()
if last_working
else None
)
@@ -1224,13 +1247,15 @@ class AsyncExecuteReportScheduleCommand(BaseCommand):
)
user = security_manager.find_user(username)
start_time = datetime.utcnow()
start_time = datetime.now(timezone.utc).replace(tzinfo=None)
with override_user(user):
ReportScheduleStateMachine(
self._execution_id, self._model, self._scheduled_dttm
).run()
elapsed_seconds = (datetime.utcnow() - start_time).total_seconds()
elapsed_seconds = (
datetime.now(timezone.utc).replace(tzinfo=None) - start_time
).total_seconds()
logger.info(
"Report execution as user %s completed in %.2fs - execution_id: %s",
username,

View File

@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy.exc import SQLAlchemyError
@@ -41,7 +41,7 @@ class AsyncPruneReportScheduleLogCommand(BaseCommand):
for report_schedule in db.session.query(ReportSchedule).all():
if report_schedule.log_retention is not None:
from_date = datetime.utcnow() - timedelta(
from_date = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(
days=report_schedule.log_retention
)
try:

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any
import humanize
@@ -142,7 +142,7 @@ class LogDAO(BaseDAO[Log]):
"item_title": item_title,
"time": datetime_to_epoch(log.dttm),
"time_delta_humanized": humanize.naturaltime(
datetime.utcnow() - log.dttm
datetime.now(timezone.utc).replace(tzinfo=None) - log.dttm
),
}
)

View File

@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Union
from superset import sql_lab
@@ -48,7 +48,9 @@ class QueryDAO(BaseDAO[Query]):
@staticmethod
def get_queries_changed_after(last_updated_ms: Union[float, int]) -> list[Query]:
# UTC date time, same that is stored in the DB.
last_updated_dt = datetime.utcfromtimestamp(last_updated_ms / 1000)
last_updated_dt = datetime.fromtimestamp(
last_updated_ms / 1000, timezone.utc
).replace(tzinfo=None)
return (
db.session.query(Query)

View File

@@ -2602,7 +2602,9 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
):
value = db_engine_spec.convert_dttm(
target_type=target_native_type,
dttm=datetime.utcfromtimestamp(value / 1000),
dttm=datetime.fromtimestamp(value / 1000, timezone.utc).replace(
tzinfo=None
),
db_extra=db_extra,
)
value = literal_column(value)

View File

@@ -18,7 +18,7 @@ from __future__ import annotations
import inspect
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from functools import wraps
from typing import Any, Callable
@@ -73,7 +73,7 @@ def set_and_log_cache(
if timeout == CACHE_DISABLED_TIMEOUT:
return
try:
dttm = datetime.utcnow().isoformat().split(".")[0]
dttm = datetime.now(timezone.utc).replace(tzinfo=None).isoformat().split(".")[0]
value = {**cache_value, "dttm": dttm}
cache_instance.set(cache_key, value, timeout=timeout)
stats_logger = app.config["STATS_LOGGER"]
@@ -226,7 +226,7 @@ def etag_cache( # noqa: C901
# Check if the cache is stale. Default the content_changed_time to now
# if we don't know when it was last modified.
content_changed_time = datetime.utcnow()
content_changed_time = datetime.now(timezone.utc).replace(tzinfo=None)
if get_last_modified:
content_changed_time = get_last_modified(*args, **kwargs)
if (

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
from datetime import datetime, timezone
import pytz
@@ -31,4 +31,4 @@ def datetime_to_epoch(dttm: datetime) -> float:
def now_as_float() -> float:
return datetime_to_epoch(datetime.utcnow())
return datetime_to_epoch(datetime.now(timezone.utc).replace(tzinfo=None))