You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jo...@apache.org on 2020/04/24 17:07:57 UTC
[incubator-superset] branch master updated: [mypy] Enforcing typing
for a number of modules (#9586)
This is an automated email from the ASF dual-hosted git repository.
johnbodley pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 1c656fe [mypy] Enforcing typing for a number of modules (#9586)
1c656fe is described below
commit 1c656feb95c15007c3c5f90b199b721c26bfa0bd
Author: John Bodley <45...@users.noreply.github.com>
AuthorDate: Fri Apr 24 10:07:35 2020 -0700
[mypy] Enforcing typing for a number of modules (#9586)
Co-authored-by: John Bodley <jo...@airbnb.com>
---
setup.cfg | 2 +-
superset/connectors/druid/models.py | 2 +
superset/exceptions.py | 2 +-
superset/models/schedules.py | 8 ++--
superset/security/analytics_db_safety.py | 3 +-
superset/security/manager.py | 7 +--
superset/sql_validators/presto_db.py | 3 +-
superset/tasks/cache.py | 32 +++++++------
superset/tasks/celery_app.py | 2 +-
superset/tasks/schedules.py | 77 +++++++++++++++++++++++---------
superset/tasks/thumbnails.py | 8 ++--
superset/utils/core.py | 54 +++++++++++-----------
12 files changed, 121 insertions(+), 79 deletions(-)
diff --git a/setup.cfg b/setup.cfg
index d58d80a..9469118 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -53,7 +53,7 @@ order_by_type = false
ignore_missing_imports = true
no_implicit_optional = true
-[mypy-superset.bin.*,superset.charts.*,superset.datasets.*,superset.dashboards.*,superset.commands.*,superset.common.*,superset.dao.*,superset.db_engine_specs.*,superset.db_engines.*,superset.examples.*,superset.migrations.*]
+[mypy-superset.bin.*,superset.charts.*,superset.datasets.*,superset.dashboards.*,superset.commands.*,superset.common.*,superset.dao.*,superset.db_engine_specs.*,superset.db_engines.*,superset.examples.*,superset.migrations.*,superset.queries.*,superset.security.*,superset.sql_validators.*,superset.tasks.*]
check_untyped_defs = true
disallow_untyped_calls = true
disallow_untyped_defs = true
diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py
index eef20e2..8b841cc 100644
--- a/superset/connectors/druid/models.py
+++ b/superset/connectors/druid/models.py
@@ -823,6 +823,7 @@ class DruidDatasource(Model, BaseDatasource):
if origin:
dttm = utils.parse_human_datetime(origin)
+ assert dttm
granularity["origin"] = dttm.isoformat()
if period_name in iso_8601_dict:
@@ -978,6 +979,7 @@ class DruidDatasource(Model, BaseDatasource):
# TODO: Use Lexicographic TopNMetricSpec once supported by PyDruid
if self.fetch_values_from:
from_dttm = utils.parse_human_datetime(self.fetch_values_from)
+ assert from_dttm
else:
from_dttm = datetime(1970, 1, 1)
diff --git a/superset/exceptions.py b/superset/exceptions.py
index e7f2e2d..33841cd 100644
--- a/superset/exceptions.py
+++ b/superset/exceptions.py
@@ -41,7 +41,7 @@ class SupersetTimeoutException(SupersetException):
class SupersetSecurityException(SupersetException):
status = 401
- def __init__(self, msg, link=None):
+ def __init__(self, msg: str, link: Optional[str] = None) -> None:
super(SupersetSecurityException, self).__init__(msg)
self.link = link
diff --git a/superset/models/schedules.py b/superset/models/schedules.py
index 5d10b56..6e2157f 100644
--- a/superset/models/schedules.py
+++ b/superset/models/schedules.py
@@ -15,8 +15,8 @@
# specific language governing permissions and limitations
# under the License.
"""Models for scheduled execution of jobs"""
-
import enum
+from typing import Optional, Type
from flask_appbuilder import Model
from sqlalchemy import Boolean, Column, Enum, ForeignKey, Integer, String, Text
@@ -86,9 +86,9 @@ class SliceEmailSchedule(Model, AuditMixinNullable, ImportMixin, EmailSchedule):
email_format = Column(Enum(SliceEmailReportFormat))
-def get_scheduler_model(report_type):
- if report_type == ScheduleType.dashboard.value:
+def get_scheduler_model(report_type: ScheduleType) -> Optional[Type[EmailSchedule]]:
+ if report_type == ScheduleType.dashboard:
return DashboardEmailSchedule
- elif report_type == ScheduleType.slice.value:
+ elif report_type == ScheduleType.slice:
return SliceEmailSchedule
return None
diff --git a/superset/security/analytics_db_safety.py b/superset/security/analytics_db_safety.py
index 64c7711..5c6a3f2 100644
--- a/superset/security/analytics_db_safety.py
+++ b/superset/security/analytics_db_safety.py
@@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+from sqlalchemy.engine.url import URL
class DBSecurityException(Exception):
@@ -22,7 +23,7 @@ class DBSecurityException(Exception):
status = 400
-def check_sqlalchemy_uri(uri):
+def check_sqlalchemy_uri(uri: URL) -> None:
if uri.startswith("sqlite"):
# sqlite creates a local DB, which allows mapping server's filesystem
raise DBSecurityException(
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 01c80d6..e3b4b1d 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -38,6 +38,7 @@ from flask_appbuilder.widgets import ListWidget
from sqlalchemy import or_
from sqlalchemy.engine.base import Connection
from sqlalchemy.orm.mapper import Mapper
+from sqlalchemy.orm.query import Query
from superset import sql_parse
from superset.connectors.connector_registry import ConnectorRegistry
@@ -70,7 +71,7 @@ class SupersetRoleListWidget(ListWidget):
template = "superset/fab_overrides/list_role.html"
- def __init__(self, **kwargs):
+ def __init__(self, **kwargs: Any) -> None:
kwargs["appbuilder"] = current_app.appbuilder
super().__init__(**kwargs)
@@ -580,7 +581,7 @@ class SupersetSecurityManager(SecurityManager):
if pv.permission and pv.view_menu:
all_pvs.add((pv.permission.name, pv.view_menu.name))
- def merge_pv(view_menu, perm):
+ def merge_pv(view_menu: str, perm: str) -> None:
"""Create permission view menu only if it doesn't exist"""
if view_menu and perm and (view_menu, perm) not in all_pvs:
self.add_permission_view_menu(view_menu, perm)
@@ -899,7 +900,7 @@ class SupersetSecurityManager(SecurityManager):
self.assert_datasource_permission(viz.datasource)
- def get_rls_filters(self, table: "BaseDatasource"):
+ def get_rls_filters(self, table: "BaseDatasource") -> List[Query]:
"""
Retrieves the appropriate row level security filters for the current user and the passed table.
diff --git a/superset/sql_validators/presto_db.py b/superset/sql_validators/presto_db.py
index fc5efda..42e7cff 100644
--- a/superset/sql_validators/presto_db.py
+++ b/superset/sql_validators/presto_db.py
@@ -23,6 +23,7 @@ from typing import Any, Dict, List, Optional
from flask import g
from superset import app, security_manager
+from superset.models.core import Database
from superset.sql_parse import ParsedQuery
from superset.sql_validators.base import BaseSQLValidator, SQLValidationAnnotation
from superset.utils.core import QuerySource
@@ -44,7 +45,7 @@ class PrestoDBSQLValidator(BaseSQLValidator):
@classmethod
def validate_statement(
- cls, statement, database, cursor, user_name
+ cls, statement: str, database: Database, cursor: Any, user_name: str
) -> Optional[SQLValidationAnnotation]:
# pylint: disable=too-many-locals
db_engine_spec = database.db_engine_spec
diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py
index 67c366b..b530deb 100644
--- a/superset/tasks/cache.py
+++ b/superset/tasks/cache.py
@@ -18,7 +18,7 @@
import json
import logging
-from typing import Any, Dict, Optional
+from typing import Any, Dict, List, Optional, Union
from urllib import request
from urllib.error import URLError
@@ -38,7 +38,9 @@ logger = get_task_logger(__name__)
logger.setLevel(logging.INFO)
-def get_form_data(chart_id, dashboard=None):
+def get_form_data(
+ chart_id: int, dashboard: Optional[Dashboard] = None
+) -> Dict[str, Any]:
"""
Build `form_data` for chart GET request from dashboard's `default_filters`.
@@ -46,7 +48,7 @@ def get_form_data(chart_id, dashboard=None):
filters in the GET request for charts.
"""
- form_data = {"slice_id": chart_id}
+ form_data: Dict[str, Any] = {"slice_id": chart_id}
if dashboard is None or not dashboard.json_metadata:
return form_data
@@ -72,7 +74,7 @@ def get_form_data(chart_id, dashboard=None):
return form_data
-def get_url(chart, extra_filters: Optional[Dict[str, Any]] = None):
+def get_url(chart: Slice, extra_filters: Optional[Dict[str, Any]] = None) -> str:
"""Return external URL for warming up a given chart/table cache."""
with app.test_request_context():
baseurl = (
@@ -106,10 +108,10 @@ class Strategy:
"""
- def __init__(self):
+ def __init__(self) -> None:
pass
- def get_urls(self):
+ def get_urls(self) -> List[str]:
raise NotImplementedError("Subclasses must implement get_urls!")
@@ -131,7 +133,7 @@ class DummyStrategy(Strategy):
name = "dummy"
- def get_urls(self):
+ def get_urls(self) -> List[str]:
session = db.create_scoped_session()
charts = session.query(Slice).all()
@@ -158,12 +160,12 @@ class TopNDashboardsStrategy(Strategy):
name = "top_n_dashboards"
- def __init__(self, top_n=5, since="7 days ago"):
+ def __init__(self, top_n: int = 5, since: str = "7 days ago") -> None:
super(TopNDashboardsStrategy, self).__init__()
self.top_n = top_n
self.since = parse_human_datetime(since)
- def get_urls(self):
+ def get_urls(self) -> List[str]:
urls = []
session = db.create_scoped_session()
@@ -203,11 +205,11 @@ class DashboardTagsStrategy(Strategy):
name = "dashboard_tags"
- def __init__(self, tags=None):
+ def __init__(self, tags: Optional[List[str]] = None) -> None:
super(DashboardTagsStrategy, self).__init__()
self.tags = tags or []
- def get_urls(self):
+ def get_urls(self) -> List[str]:
urls = []
session = db.create_scoped_session()
@@ -254,7 +256,9 @@ strategies = [DummyStrategy, TopNDashboardsStrategy, DashboardTagsStrategy]
@celery_app.task(name="cache-warmup")
-def cache_warmup(strategy_name, *args, **kwargs):
+def cache_warmup(
+ strategy_name: str, *args: Any, **kwargs: Any
+) -> Union[Dict[str, List[str]], str]:
"""
Warm up cache.
@@ -264,7 +268,7 @@ def cache_warmup(strategy_name, *args, **kwargs):
logger.info("Loading strategy")
class_ = None
for class_ in strategies:
- if class_.name == strategy_name:
+ if class_.name == strategy_name: # type: ignore
break
else:
message = f"No strategy {strategy_name} found!"
@@ -280,7 +284,7 @@ def cache_warmup(strategy_name, *args, **kwargs):
logger.exception(message)
return message
- results = {"success": [], "errors": []}
+ results: Dict[str, List[str]] = {"success": [], "errors": []}
for url in strategy.get_urls():
try:
logger.info(f"Fetching {url}")
diff --git a/superset/tasks/celery_app.py b/superset/tasks/celery_app.py
index 0f3cd0e..0344b59 100644
--- a/superset/tasks/celery_app.py
+++ b/superset/tasks/celery_app.py
@@ -25,7 +25,7 @@ from superset import create_app
from superset.extensions import celery_app
# Init the Flask app / configure everything
-create_app()
+create_app() # type: ignore
# Need to import late, as the celery_app will have been setup by "create_app()"
# pylint: disable=wrong-import-position, unused-import
diff --git a/superset/tasks/schedules.py b/superset/tasks/schedules.py
index 3889d02..45036a8 100644
--- a/superset/tasks/schedules.py
+++ b/superset/tasks/schedules.py
@@ -23,10 +23,12 @@ import urllib.request
from collections import namedtuple
from datetime import datetime, timedelta
from email.utils import make_msgid, parseaddr
+from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
from urllib.error import URLError # pylint: disable=ungrouped-imports
import croniter
import simplejson as json
+from celery.app.task import Task
from dateutil.tz import tzlocal
from flask import render_template, Response, session, url_for
from flask_babel import gettext as __
@@ -34,16 +36,20 @@ from flask_login import login_user
from retry.api import retry_call
from selenium.common.exceptions import WebDriverException
from selenium.webdriver import chrome, firefox
+from werkzeug.datastructures import TypeConversionDict
from werkzeug.http import parse_cookie
# Superset framework imports
from superset import app, db, security_manager
from superset.extensions import celery_app
from superset.models.schedules import (
+ DashboardEmailSchedule,
EmailDeliveryType,
+ EmailSchedule,
get_scheduler_model,
ScheduleType,
SliceEmailReportFormat,
+ SliceEmailSchedule,
)
from superset.utils.core import get_email_address_list, send_email_smtp
@@ -59,7 +65,9 @@ PAGE_RENDER_WAIT = 30
EmailContent = namedtuple("EmailContent", ["body", "data", "images"])
-def _get_recipients(schedule):
+def _get_recipients(
+ schedule: Union[DashboardEmailSchedule, SliceEmailSchedule]
+) -> Iterator[Tuple[str, str]]:
bcc = config["EMAIL_REPORT_BCC_ADDRESS"]
if schedule.deliver_as_group:
@@ -70,7 +78,11 @@ def _get_recipients(schedule):
yield (to, bcc)
-def _deliver_email(schedule, subject, email):
+def _deliver_email(
+ schedule: Union[DashboardEmailSchedule, SliceEmailSchedule],
+ subject: str,
+ email: EmailContent,
+) -> None:
for (to, bcc) in _get_recipients(schedule):
send_email_smtp(
to,
@@ -85,7 +97,11 @@ def _deliver_email(schedule, subject, email):
)
-def _generate_mail_content(schedule, screenshot, name, url):
+def _generate_mail_content(
+ schedule: EmailSchedule, screenshot: bytes, name: str, url: str
+) -> EmailContent:
+ data: Optional[Dict[str, Any]]
+
if schedule.delivery_type == EmailDeliveryType.attachment:
images = None
data = {"screenshot.png": screenshot}
@@ -115,7 +131,7 @@ def _generate_mail_content(schedule, screenshot, name, url):
return EmailContent(body, data, images)
-def _get_auth_cookies():
+def _get_auth_cookies() -> List[TypeConversionDict]:
# Login with the user specified to get the reports
with app.test_request_context():
user = security_manager.find_user(config["EMAIL_REPORTS_USER"])
@@ -136,14 +152,16 @@ def _get_auth_cookies():
return cookies
-def _get_url_path(view, **kwargs):
+def _get_url_path(view: str, **kwargs: Any) -> str:
with app.test_request_context():
return urllib.parse.urljoin(
str(config["WEBDRIVER_BASEURL"]), url_for(view, **kwargs)
)
-def create_webdriver():
+def create_webdriver() -> Union[
+ chrome.webdriver.WebDriver, firefox.webdriver.WebDriver
+]:
# Create a webdriver for use in fetching reports
if config["EMAIL_REPORTS_WEBDRIVER"] == "firefox":
driver_class = firefox.webdriver.WebDriver
@@ -181,7 +199,9 @@ def create_webdriver():
return driver
-def destroy_webdriver(driver):
+def destroy_webdriver(
+ driver: Union[chrome.webdriver.WebDriver, firefox.webdriver.WebDriver]
+) -> None:
"""
Destroy a driver
"""
@@ -198,7 +218,7 @@ def destroy_webdriver(driver):
pass
-def deliver_dashboard(schedule):
+def deliver_dashboard(schedule: DashboardEmailSchedule) -> None:
"""
Given a schedule, delivery the dashboard as an email report
"""
@@ -243,7 +263,7 @@ def deliver_dashboard(schedule):
_deliver_email(schedule, subject, email)
-def _get_slice_data(schedule):
+def _get_slice_data(schedule: SliceEmailSchedule) -> EmailContent:
slc = schedule.slice
slice_url = _get_url_path(
@@ -272,7 +292,7 @@ def _get_slice_data(schedule):
# Parse the csv file and generate HTML
columns = rows.pop(0)
- with app.app_context():
+ with app.app_context(): # type: ignore
body = render_template(
"superset/reports/slice_data.html",
columns=columns,
@@ -292,7 +312,7 @@ def _get_slice_data(schedule):
return EmailContent(body, data, None)
-def _get_slice_visualization(schedule):
+def _get_slice_visualization(schedule: SliceEmailSchedule) -> EmailContent:
slc = schedule.slice
# Create a driver, fetch the page, wait for the page to render
@@ -327,7 +347,7 @@ def _get_slice_visualization(schedule):
return _generate_mail_content(schedule, screenshot, slc.slice_name, slice_url)
-def deliver_slice(schedule):
+def deliver_slice(schedule: Union[DashboardEmailSchedule, SliceEmailSchedule]) -> None:
"""
Given a schedule, delivery the slice as an email report
"""
@@ -352,9 +372,12 @@ def deliver_slice(schedule):
bind=True,
soft_time_limit=config["EMAIL_ASYNC_TIME_LIMIT_SEC"],
)
-def schedule_email_report(
- task, report_type, schedule_id, recipients=None
-): # pylint: disable=unused-argument
+def schedule_email_report( # pylint: disable=unused-argument
+ task: Task,
+ report_type: ScheduleType,
+ schedule_id: int,
+ recipients: Optional[str] = None,
+) -> None:
model_cls = get_scheduler_model(report_type)
schedule = db.create_scoped_session().query(model_cls).get(schedule_id)
@@ -368,15 +391,17 @@ def schedule_email_report(
schedule.id = schedule_id
schedule.recipients = recipients
- if report_type == ScheduleType.dashboard.value:
+ if report_type == ScheduleType.dashboard:
deliver_dashboard(schedule)
- elif report_type == ScheduleType.slice.value:
+ elif report_type == ScheduleType.slice:
deliver_slice(schedule)
else:
raise RuntimeError("Unknown report type")
-def next_schedules(crontab, start_at, stop_at, resolution=0):
+def next_schedules(
+ crontab: str, start_at: datetime, stop_at: datetime, resolution: int = 0
+) -> Iterator[datetime]:
crons = croniter.croniter(crontab, start_at - timedelta(seconds=1))
previous = start_at - timedelta(days=1)
@@ -396,13 +421,19 @@ def next_schedules(crontab, start_at, stop_at, resolution=0):
previous = eta
-def schedule_window(report_type, start_at, stop_at, resolution):
+def schedule_window(
+ report_type: ScheduleType, start_at: datetime, stop_at: datetime, resolution: int
+) -> None:
"""
Find all active schedules and schedule celery tasks for
each of them with a specific ETA (determined by parsing
the cron schedule for the schedule)
"""
model_cls = get_scheduler_model(report_type)
+
+ if not model_cls:
+ return None
+
dbsession = db.create_scoped_session()
schedules = dbsession.query(model_cls).filter(model_cls.active.is_(True))
@@ -415,9 +446,11 @@ def schedule_window(report_type, start_at, stop_at, resolution):
):
schedule_email_report.apply_async(args, eta=eta)
+ return None
+
@celery_app.task(name="email_reports.schedule_hourly")
-def schedule_hourly():
+def schedule_hourly() -> None:
""" Celery beat job meant to be invoked hourly """
if not config["ENABLE_SCHEDULED_EMAIL_REPORTS"]:
@@ -429,5 +462,5 @@ def schedule_hourly():
# Get the top of the hour
start_at = datetime.now(tzlocal()).replace(microsecond=0, second=0, minute=0)
stop_at = start_at + timedelta(seconds=3600)
- schedule_window(ScheduleType.dashboard.value, start_at, stop_at, resolution)
- schedule_window(ScheduleType.slice.value, start_at, stop_at, resolution)
+ schedule_window(ScheduleType.dashboard, start_at, stop_at, resolution)
+ schedule_window(ScheduleType.slice, start_at, stop_at, resolution)
diff --git a/superset/tasks/thumbnails.py b/superset/tasks/thumbnails.py
index 72c7bda..1197700 100644
--- a/superset/tasks/thumbnails.py
+++ b/superset/tasks/thumbnails.py
@@ -30,8 +30,8 @@ logger = logging.getLogger(__name__)
@celery_app.task(name="cache_chart_thumbnail", soft_time_limit=300)
-def cache_chart_thumbnail(chart_id: int, force: bool = False):
- with app.app_context():
+def cache_chart_thumbnail(chart_id: int, force: bool = False) -> None:
+ with app.app_context(): # type: ignore
if not thumbnail_cache:
logger.warning("No cache set, refusing to compute")
return None
@@ -42,8 +42,8 @@ def cache_chart_thumbnail(chart_id: int, force: bool = False):
@celery_app.task(name="cache_dashboard_thumbnail", soft_time_limit=300)
-def cache_dashboard_thumbnail(dashboard_id: int, force: bool = False):
- with app.app_context():
+def cache_dashboard_thumbnail(dashboard_id: int, force: bool = False) -> None:
+ with app.app_context(): # type: ignore
if not thumbnail_cache:
logging.warning("No cache set, refusing to compute")
return None
diff --git a/superset/utils/core.py b/superset/utils/core.py
index ba715dd..41deae5 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -235,7 +235,7 @@ def list_minus(l: List, minus: List) -> List:
return [o for o in l if o not in minus]
-def parse_human_datetime(s):
+def parse_human_datetime(s: Optional[str]) -> Optional[datetime]:
"""
Returns ``datetime.datetime`` from human readable strings
@@ -687,42 +687,42 @@ def notify_user_about_perm_udate(granter, user, role, datasource, tpl_name, conf
def send_email_smtp(
- to,
- subject,
- html_content,
- config,
- files=None,
- data=None,
- images=None,
- dryrun=False,
- cc=None,
- bcc=None,
- mime_subtype="mixed",
-):
+ to: str,
+ subject: str,
+ html_content: str,
+ config: Dict[str, Any],
+ files: Optional[List[str]] = None,
+ data: Optional[Dict[str, str]] = None,
+ images: Optional[Dict[str, str]] = None,
+ dryrun: bool = False,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None,
+ mime_subtype: str = "mixed",
+) -> None:
"""
Send an email with html content, eg:
send_email_smtp(
'test@example.com', 'foo', '<b>Foo</b> bar',['/dev/null'], dryrun=True)
"""
smtp_mail_from = config["SMTP_MAIL_FROM"]
- to = get_email_address_list(to)
+ smtp_mail_to = get_email_address_list(to)
msg = MIMEMultipart(mime_subtype)
msg["Subject"] = subject
msg["From"] = smtp_mail_from
- msg["To"] = ", ".join(to)
+ msg["To"] = ", ".join(smtp_mail_to)
msg.preamble = "This is a multi-part message in MIME format."
- recipients = to
+ recipients = smtp_mail_to
if cc:
- cc = get_email_address_list(cc)
- msg["CC"] = ", ".join(cc)
- recipients = recipients + cc
+ smtp_mail_cc = get_email_address_list(cc)
+ msg["CC"] = ", ".join(smtp_mail_cc)
+ recipients = recipients + smtp_mail_cc
if bcc:
# don't add bcc in header
- bcc = get_email_address_list(bcc)
- recipients = recipients + bcc
+ smtp_mail_bcc = get_email_address_list(bcc)
+ recipients = recipients + smtp_mail_bcc
msg["Date"] = formatdate(localtime=True)
mime_text = MIMEText(html_content, "html")
@@ -1034,8 +1034,8 @@ def get_since_until(
"""
separator = " : "
- relative_start = parse_human_datetime(relative_start if relative_start else "today")
- relative_end = parse_human_datetime(relative_end if relative_end else "today")
+ relative_start = parse_human_datetime(relative_start if relative_start else "today") # type: ignore
+ relative_end = parse_human_datetime(relative_end if relative_end else "today") # type: ignore
common_time_frames = {
"Last day": (
relative_start - relativedelta(days=1), # type: ignore
@@ -1064,8 +1064,8 @@ def get_since_until(
since, until = time_range.split(separator, 1)
if since and since not in common_time_frames:
since = add_ago_to_since(since)
- since = parse_human_datetime(since)
- until = parse_human_datetime(until)
+ since = parse_human_datetime(since) # type: ignore
+ until = parse_human_datetime(until) # type: ignore
elif time_range in common_time_frames:
since, until = common_time_frames[time_range]
elif time_range == "No filter":
@@ -1086,8 +1086,8 @@ def get_since_until(
since = since or ""
if since:
since = add_ago_to_since(since)
- since = parse_human_datetime(since)
- until = parse_human_datetime(until) if until else relative_end
+ since = parse_human_datetime(since) # type: ignore
+ until = parse_human_datetime(until) if until else relative_end # type: ignore
if time_shift:
time_delta = parse_past_timedelta(time_shift)