From bba92779469c8f0fa56a0db0f53a2d29a579df42 Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Wed, 12 Oct 2022 21:48:57 +0800 Subject: [PATCH] should pass all CI steps --- .../src/sections/sections.tsx | 10 +-- .../superset-ui-core/src/query/index.ts | 2 +- .../test/query/buildQueryContext.test.ts | 59 ++++++++++++---- .../test/query/getAxis.test.ts | 55 ++------------- superset/common/query_context_processor.py | 24 ++++--- superset/common/query_object.py | 8 +-- superset/common/query_object_factory.py | 27 ++------ superset/common/utils/time_range_utils.py | 69 +++++++++++++++++++ superset/connectors/sqla/models.py | 10 ++- superset/constants.py | 2 + superset/db_engine_specs/sqlite.py | 6 +- superset/models/helpers.py | 3 +- superset/utils/core.py | 15 ++-- superset/utils/date_parser.py | 2 +- superset/viz.py | 2 +- tests/integration_tests/utils_tests.py | 10 +-- 16 files changed, 183 insertions(+), 121 deletions(-) create mode 100644 superset/common/utils/time_range_utils.py diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/sections.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/sections.tsx index 6487d5add065d..4535f1996dd72 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/sections.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/sections.tsx @@ -49,10 +49,12 @@ export const genericTime: ControlPanelSectionConfig = hasGenericChartAxes ], }; -export const legacyRegularTime: ControlPanelSectionConfig = { - ...baseTimeSection, - controlSetRows: [['granularity_sqla'], ['time_range']], -}; +export const legacyRegularTime: ControlPanelSectionConfig = hasGenericChartAxes + ? { controlSetRows: [] } + : { + ...baseTimeSection, + controlSetRows: [['granularity_sqla'], ['time_range']], + }; export const datasourceAndVizType: ControlPanelSectionConfig = { label: t('Datasource & Chart Type'), diff --git a/superset-frontend/packages/superset-ui-core/src/query/index.ts b/superset-frontend/packages/superset-ui-core/src/query/index.ts index 21c775ad6b4c5..9909daf062ada 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/index.ts @@ -29,7 +29,7 @@ export { default as getMetricLabel } from './getMetricLabel'; export { default as DatasourceKey } from './DatasourceKey'; export { default as normalizeOrderBy } from './normalizeOrderBy'; export { normalizeTimeColumn } from './normalizeTimeColumn'; -export { getXAxis, isXAxisSet, hasGenericChartAxes } from './getXAxis'; +export * from './getXAxis'; export * from './types/AnnotationLayer'; export * from './types/QueryFormData'; diff --git a/superset-frontend/packages/superset-ui-core/test/query/buildQueryContext.test.ts b/superset-frontend/packages/superset-ui-core/test/query/buildQueryContext.test.ts index 4a9e71a6dd89e..f5b370e32a7a2 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/buildQueryContext.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/buildQueryContext.test.ts @@ -18,6 +18,7 @@ */ import { buildQueryContext } from '@superset-ui/core'; import * as queryModule from '../../src/query/normalizeTimeColumn'; +import * as getXAxisModule from '../../src/query/getXAxis'; describe('buildQueryContext', () => { it('should build datasource for table sources and apply defaults', () => { @@ -98,6 +99,7 @@ describe('buildQueryContext', () => { ]), ); }); + // todo(Yongjie): move these test case into buildQueryObject.test.ts it('should remove undefined value in post_processing', () => { const queryContext = buildQueryContext( { @@ -124,12 +126,9 @@ describe('buildQueryContext', () => { ]); }); it('should call normalizeTimeColumn if GENERIC_CHART_AXES is enabled and has x_axis', () => { - // @ts-ignore - const spy = jest.spyOn(window, 'window', 'get').mockImplementation(() => ({ - featureFlags: { - GENERIC_CHART_AXES: true, - }, - })); + Object.defineProperty(getXAxisModule, 'hasGenericChartAxes', { + value: true, + }); const spyNormalizeTimeColumn = jest.spyOn( queryModule, 'normalizeTimeColumn', @@ -144,16 +143,12 @@ describe('buildQueryContext', () => { () => [{}], ); expect(spyNormalizeTimeColumn).toBeCalled(); - spy.mockRestore(); spyNormalizeTimeColumn.mockRestore(); }); it("shouldn't call normalizeTimeColumn if GENERIC_CHART_AXES is disabled", () => { - // @ts-ignore - const spy = jest.spyOn(window, 'window', 'get').mockImplementation(() => ({ - featureFlags: { - GENERIC_CHART_AXES: false, - }, - })); + Object.defineProperty(getXAxisModule, 'hasGenericChartAxes', { + value: false, + }); const spyNormalizeTimeColumn = jest.spyOn( queryModule, 'normalizeTimeColumn', @@ -167,7 +162,43 @@ describe('buildQueryContext', () => { () => [{}], ); expect(spyNormalizeTimeColumn).not.toBeCalled(); - spy.mockRestore(); spyNormalizeTimeColumn.mockRestore(); }); + it('should orverride time filter if GENERIC_CHART_AXES is enabled', () => { + Object.defineProperty(getXAxisModule, 'hasGenericChartAxes', { + value: true, + }); + + const queryContext = buildQueryContext( + { + datasource: '5__table', + viz_type: 'table', + }, + () => [ + { + filters: [ + { + col: 'col1', + op: 'DATETIME_BETWEEN', + val: '2001 : 2002', + }, + { + col: 'col2', + op: 'IN', + val: ['a', 'b'], + }, + ], + time_range: '1990 : 1991', + }, + ], + ); + expect(queryContext.queries[0].filters).toEqual([ + { col: 'col1', op: 'DATETIME_BETWEEN', val: '1990 : 1991' }, + { + col: 'col2', + op: 'IN', + val: ['a', 'b'], + }, + ]); + }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/query/getAxis.test.ts b/superset-frontend/packages/superset-ui-core/test/query/getAxis.test.ts index 6db1c150eaf33..010bd9fc67591 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/getAxis.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/getAxis.test.ts @@ -18,54 +18,9 @@ */ import { isXAxisSet } from '@superset-ui/core'; -describe('GENERIC_CHART_AXES is enabled', () => { - let windowSpy: any; - - beforeAll(() => { - // @ts-ignore - windowSpy = jest.spyOn(window, 'window', 'get').mockImplementation(() => ({ - featureFlags: { - GENERIC_CHART_AXES: true, - }, - })); - }); - - afterAll(() => { - windowSpy.mockRestore(); - }); - - it('isEnabledAxies when FF is disabled', () => { - expect( - isXAxisSet({ datasource: '123', viz_type: 'table' }), - ).not.toBeTruthy(); - expect( - isXAxisSet({ datasource: '123', viz_type: 'table', x_axis: 'axis' }), - ).toBeTruthy(); - }); -}); - -describe('GENERIC_CHART_AXES is disabled', () => { - let windowSpy: any; - - beforeAll(() => { - // @ts-ignore - windowSpy = jest.spyOn(window, 'window', 'get').mockImplementation(() => ({ - featureFlags: { - GENERIC_CHART_AXES: false, - }, - })); - }); - - afterAll(() => { - windowSpy.mockRestore(); - }); - - it('isEnabledAxies when FF is disabled', () => { - expect( - isXAxisSet({ datasource: '123', viz_type: 'table' }), - ).not.toBeTruthy(); - expect( - isXAxisSet({ datasource: '123', viz_type: 'table', x_axis: 'axis' }), - ).toBeTruthy(); - }); +test('isXAxisSet', () => { + expect(isXAxisSet({ datasource: '123', viz_type: 'table' })).not.toBeTruthy(); + expect( + isXAxisSet({ datasource: '123', viz_type: 'table', x_axis: 'axis' }), + ).toBeTruthy(); }); diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index 01259ede1d8a5..d193dfa813085 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -35,6 +35,7 @@ from superset.common.query_actions import get_query_results from superset.common.utils import dataframe_utils from superset.common.utils.query_cache_manager import QueryCacheManager +from superset.common.utils.time_range_utils import get_since_until_from_query_object from superset.connectors.base.models import BaseDatasource from superset.constants import CacheRegion from superset.exceptions import ( @@ -56,6 +57,7 @@ get_column_names_from_columns, get_column_names_from_metrics, get_metric_names, + get_xaxis_label, normalize_dttm_col, TIME_COMPARISON, ) @@ -314,8 +316,14 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme rv_dfs: List[pd.DataFrame] = [df] time_offsets = query_object.time_offsets - outer_from_dttm = query_object.from_dttm - outer_to_dttm = query_object.to_dttm + outer_from_dttm, outer_to_dttm = get_since_until_from_query_object(query_object) + if not outer_from_dttm or not outer_to_dttm: + raise QueryObjectValidationError( + _( + "An enclosed time range (both start and end) must be specified " + "when using a Time Comparison." + ) + ) for offset in time_offsets: try: query_object_clone.from_dttm = get_past_or_future( @@ -330,14 +338,12 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme query_object_clone.inner_to_dttm = outer_to_dttm query_object_clone.time_offsets = [] query_object_clone.post_processing = [] + query_object_clone.filter = [ + flt + for flt in query_object_clone.filter + if flt.get("col") != get_xaxis_label(query_object.columns) + ] - if not query_object.from_dttm or not query_object.to_dttm: - raise QueryObjectValidationError( - _( - "An enclosed time range (both start and end) must be specified " - "when using a Time Comparison." - ) - ) # `offset` is added to the hash function cache_key = self.query_cache_key(query_object_clone, time_offset=offset) cache = QueryCacheManager.get( diff --git a/superset/common/query_object.py b/superset/common/query_object.py index ac86273b27098..94cf2a74ccaa9 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -19,7 +19,7 @@ import json import logging -from datetime import datetime, timedelta +from datetime import datetime from pprint import pformat from typing import Any, Dict, List, NamedTuple, Optional, TYPE_CHECKING @@ -46,7 +46,6 @@ json_int_dttm_ser, QueryObjectFilterClause, ) -from superset.utils.date_parser import parse_human_timedelta from superset.utils.hashing import md5_sha_from_dict if TYPE_CHECKING: @@ -106,7 +105,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes series_limit: int series_limit_metric: Optional[Metric] time_offsets: List[str] - time_shift: Optional[timedelta] + time_shift: Optional[str] time_range: Optional[str] to_dttm: Optional[datetime] @@ -156,7 +155,7 @@ def __init__( # pylint: disable=too-many-locals self.series_limit = series_limit self.series_limit_metric = series_limit_metric self.time_range = time_range - self.time_shift = parse_human_timedelta(time_shift) + self.time_shift = time_shift self.from_dttm = kwargs.get("from_dttm") self.to_dttm = kwargs.get("to_dttm") self.result_type = kwargs.get("result_type") @@ -336,6 +335,7 @@ def to_dict(self) -> Dict[str, Any]: "series_limit": self.series_limit, "series_limit_metric": self.series_limit_metric, "to_dttm": self.to_dttm, + "time_shift": self.time_shift, } return query_object_dict diff --git a/superset/common/query_object_factory.py b/superset/common/query_object_factory.py index 617b096ef5962..88cc7ca1b461b 100644 --- a/superset/common/query_object_factory.py +++ b/superset/common/query_object_factory.py @@ -16,14 +16,12 @@ # under the License. from __future__ import annotations -from datetime import datetime -from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING +from typing import Any, Dict, Optional, TYPE_CHECKING -from superset import app from superset.common.chart_data import ChartDataResultType from superset.common.query_object import QueryObject +from superset.common.utils.time_range_utils import get_since_until_from_time_range from superset.utils.core import apply_max_row_limit, DatasourceDict, DatasourceType -from superset.utils.date_parser import get_since_until if TYPE_CHECKING: from sqlalchemy.orm import sessionmaker @@ -63,7 +61,9 @@ def create( # pylint: disable=too-many-arguments processed_extras = self._process_extras(extras) result_type = kwargs.setdefault("result_type", parent_result_type) row_limit = self._process_row_limit(row_limit, result_type) - from_dttm, to_dttm = self._get_dttms(time_range, time_shift, processed_extras) + from_dttm, to_dttm = get_since_until_from_time_range( + time_range, time_shift, processed_extras + ) kwargs["from_dttm"] = from_dttm kwargs["to_dttm"] = to_dttm return QueryObject( @@ -99,23 +99,6 @@ def _process_row_limit( ) return apply_max_row_limit(row_limit or default_row_limit) - @staticmethod - def _get_dttms( - time_range: Optional[str], - time_shift: Optional[str] = None, - extras: Optional[Dict[str, Any]] = None, - ) -> Tuple[Optional[datetime], Optional[datetime]]: - return get_since_until( - relative_start=(extras or {}).get( - "relative_start", app.config["DEFAULT_RELATIVE_START_TIME"] - ), - relative_end=(extras or {}).get( - "relative_end", app.config["DEFAULT_RELATIVE_END_TIME"] - ), - time_range=time_range, - time_shift=time_shift, - ) - # light version of the view.utils.core # import view.utils require application context # Todo: move it and the view.utils.core to utils package diff --git a/superset/common/utils/time_range_utils.py b/superset/common/utils/time_range_utils.py new file mode 100644 index 0000000000000..3058c326c07d5 --- /dev/null +++ b/superset/common/utils/time_range_utils.py @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from datetime import datetime +from typing import Any, cast, Dict, Optional, Tuple + +from superset import app +from superset.common.query_object import QueryObject +from superset.utils.core import FilterOperator, get_xaxis_label +from superset.utils.date_parser import get_since_until + + +def get_since_until_from_time_range( + time_range: Optional[str] = None, + time_shift: Optional[str] = None, + extras: Optional[Dict[str, Any]] = None, +) -> Tuple[Optional[datetime], Optional[datetime]]: + return get_since_until( + relative_start=(extras or {}).get( + "relative_start", app.config["DEFAULT_RELATIVE_START_TIME"] + ), + relative_end=(extras or {}).get( + "relative_end", app.config["DEFAULT_RELATIVE_END_TIME"] + ), + time_range=time_range, + time_shift=time_shift, + ) + + +# pylint: disable=invalid-name +def get_since_until_from_query_object( + query_object: QueryObject, +) -> Tuple[Optional[datetime], Optional[datetime]]: + if query_object.time_range: + return get_since_until_from_time_range( + time_range=query_object.time_range, + time_shift=query_object.time_shift, + extras=query_object.extras, + ) + + time_range = None + for flt in query_object.filter: + if ( + flt.get("op") == FilterOperator.DATETIME_BETWEEN.value + and flt.get("col") == get_xaxis_label(query_object.columns) + and isinstance(flt.get("val"), str) + ): + time_range = cast(str, flt.get("val")) + + return get_since_until_from_time_range( + time_range=time_range, + time_shift=query_object.time_shift, + extras=query_object.extras, + ) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 20d257e1b9b56..73d850d961028 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -80,7 +80,7 @@ from superset.advanced_data_type.types import AdvancedDataTypeResponse from superset.columns.models import Column as NewColumn, UNKOWN_TYPE from superset.common.db_query_status import QueryStatus -from superset.common.query_object_factory import QueryObjectFactory +from superset.common.utils.time_range_utils import get_since_until_from_time_range from superset.connectors.base.models import BaseColumn, BaseDatasource, BaseMetric from superset.connectors.sqla.utils import ( find_cached_objects_in_session, @@ -1268,6 +1268,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma row_offset: Optional[int] = None, timeseries_limit: Optional[int] = None, timeseries_limit_metric: Optional[Metric] = None, + time_shift: Optional[str] = None, ) -> SqlaQuery: """Querying any sqla table from this common interface""" if granularity not in self.dttm_cols and granularity is not None: @@ -1662,10 +1663,13 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma and isinstance(eq, str) and col_obj is not None ): - _since, _until = QueryObjectFactory._get_dttms(time_range=eq) + _since, _until = get_since_until_from_time_range( + time_range=eq, + time_shift=time_shift, + extras=extras, + ) where_clause_and.append( col_obj.get_time_filter( - # set label as `sqla_col.key` is from `make_sqla_column_compatible` start_dttm=_since, end_dttm=_until, label=sqla_col.key, diff --git a/superset/constants.py b/superset/constants.py index dbd34767b705f..7d759acf6741c 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -32,6 +32,8 @@ PASSWORD_MASK = "X" * 10 +NO_TIME_RANGE = "No filter" + class RouteMethod: # pylint: disable=too-few-public-methods """ diff --git a/superset/db_engine_specs/sqlite.py b/superset/db_engine_specs/sqlite.py index 8c583060d293b..85442aa877363 100644 --- a/superset/db_engine_specs/sqlite.py +++ b/superset/db_engine_specs/sqlite.py @@ -77,7 +77,11 @@ def convert_dttm( cls, target_type: str, dttm: datetime, db_extra: Optional[Dict[str, Any]] = None ) -> Optional[str]: tt = target_type.upper() - if tt in (utils.TemporalType.TEXT, utils.TemporalType.DATETIME): + if tt in ( + utils.TemporalType.TEXT, + utils.TemporalType.DATETIME, + utils.TemporalType.TIMESTAMP, + ): return f"""'{dttm.isoformat(sep=" ", timespec="seconds")}'""" return None diff --git a/superset/models/helpers.py b/superset/models/helpers.py index da526b559c7f1..cb314de80275c 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -1322,7 +1322,7 @@ def get_sqla_col(self, col: Dict[str, Any]) -> Column: col = sa.column(label, type_=col_type) return self.make_sqla_column_compatible(col, label) - def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements + def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements,unused-argument self, apply_fetch_values_predicate: bool = False, columns: Optional[List[Column]] = None, @@ -1348,6 +1348,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma row_offset: Optional[int] = None, timeseries_limit: Optional[int] = None, timeseries_limit_metric: Optional[Metric] = None, + time_shift: Optional[str] = None, ) -> SqlaQuery: """Querying any sqla table from this common interface""" if granularity not in self.dttm_cols and granularity is not None: diff --git a/superset/utils/core.py b/superset/utils/core.py index 1a3096e9c6860..cd31d68a11b52 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -96,6 +96,7 @@ EXTRA_FORM_DATA_APPEND_KEYS, EXTRA_FORM_DATA_OVERRIDE_EXTRA_KEYS, EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS, + NO_TIME_RANGE, ) from superset.errors import ErrorLevel, SupersetErrorType from superset.exceptions import ( @@ -115,6 +116,7 @@ Metric, ) from superset.utils.database import get_example_database +from superset.utils.date_parser import parse_human_timedelta from superset.utils.dates import datetime_to_epoch, EPOCH from superset.utils.hashing import md5_sha_from_dict, md5_sha_from_str @@ -131,8 +133,6 @@ DTTM_ALIAS = "__timestamp" -NO_TIME_RANGE = "No filter" - TIME_COMPARISON = "__" JS_MAX_INTEGER = 9007199254740991 # Largest int Java Script can handle 2^53-1 @@ -1286,6 +1286,11 @@ def get_base_axis_labels(columns: Optional[List[Column]]) -> Tuple[str, ...]: return tuple(get_column_name(col) for col in axis_cols) +def get_xaxis_label(columns: Optional[List[Column]]) -> Optional[str]: + labels = get_base_axis_labels(columns) + return labels[0] if labels else None + + def get_column_name( column: Column, verbose_map: Optional[Dict[str, Any]] = None ) -> str: @@ -1857,7 +1862,7 @@ class DateColumn: col_label: str timestamp_format: Optional[str] = None offset: Optional[int] = None - time_shift: Optional[timedelta] = None + time_shift: Optional[str] = None def __hash__(self) -> int: return hash(self.col_label) @@ -1870,7 +1875,7 @@ def get_legacy_time_column( cls, timestamp_format: Optional[str], offset: Optional[int], - time_shift: Optional[timedelta], + time_shift: Optional[str], ) -> DateColumn: return cls( timestamp_format=timestamp_format, @@ -1909,7 +1914,7 @@ def normalize_dttm_col( if _col.offset: df[_col.col_label] += timedelta(hours=_col.offset) if _col.time_shift is not None: - df[_col.col_label] += _col.time_shift + df[_col.col_label] += parse_human_timedelta(_col.time_shift) def parse_boolean_string(bool_str: Optional[str]) -> bool: diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py index cc0693770bfa7..f0a525570a049 100644 --- a/superset/utils/date_parser.py +++ b/superset/utils/date_parser.py @@ -45,7 +45,7 @@ TimeRangeAmbiguousError, TimeRangeParseFailError, ) -from superset.utils.core import NO_TIME_RANGE +from superset.constants import NO_TIME_RANGE from superset.utils.memoized import memoized ParserElement.enablePackrat() diff --git a/superset/viz.py b/superset/viz.py index 43e71b533c61c..b91fdc3aaf0ff 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -307,7 +307,7 @@ def get_df(self, query_obj: Optional[QueryObjectDict] = None) -> pd.DataFrame: DateColumn.get_legacy_time_column( timestamp_format=timestamp_format, offset=self.datasource.offset, - time_shift=self.time_shift, + time_shift=self.form_data.get("time_shift"), ) ] ), diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py index 7df9cd82f96f0..9c3bac1c5b0ea 100644 --- a/tests/integration_tests/utils_tests.py +++ b/tests/integration_tests/utils_tests.py @@ -39,6 +39,7 @@ import tests.integration_tests.test_app from superset import app, db, security_manager +from superset.constants import NO_TIME_RANGE from superset.exceptions import CertificateException, SupersetException from superset.models.core import Database, Log from superset.models.dashboard import Dashboard @@ -62,7 +63,6 @@ merge_extra_filters, merge_extra_form_data, merge_request_params, - NO_TIME_RANGE, normalize_dttm_col, parse_ssl_cert, parse_js_uri_path_item, @@ -1060,7 +1060,7 @@ def normalize_col( df: pd.DataFrame, timestamp_format: Optional[str], offset: int, - time_shift: Optional[timedelta], + time_shift: Optional[str], ) -> pd.DataFrame: df = df.copy() normalize_dttm_col( @@ -1091,9 +1091,9 @@ def normalize_col( ) # test offset and timedelta - assert normalize_col(df, None, 1, timedelta(minutes=30))[DTTM_ALIAS][ - 0 - ] == pd.Timestamp(2021, 2, 15, 20, 30, 0, 0) + assert normalize_col(df, None, 1, "30 minutes")[DTTM_ALIAS][0] == pd.Timestamp( + 2021, 2, 15, 20, 30, 0, 0 + ) # test numeric epoch_s format df = pd.DataFrame([{"__timestamp": ts.timestamp(), "a": 1}])