You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2023/08/20 18:12:30 UTC
[airflow] branch main updated: Set strict to True when parsing dates in webserver views (#33512)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 4390524a41 Set strict to True when parsing dates in webserver views (#33512)
4390524a41 is described below
commit 4390524a41fdfd2d57f1d2dc98ad7b4009c8399e
Author: Hussein Awala <hu...@awala.fr>
AuthorDate: Sun Aug 20 20:12:21 2023 +0200
Set strict to True when parsing dates in webserver views (#33512)
Co-authored-by: Tzu-ping Chung <ur...@gmail.com>
---------
Co-authored-by: Tzu-ping Chung <ur...@gmail.com>
---
airflow/utils/timezone.py | 5 +++--
airflow/www/views.py | 11 ++++++-----
tests/www/views/test_views_extra_links.py | 22 ++++++++++++----------
tests/www/views/test_views_log.py | 3 ++-
tests/www/views/test_views_tasks.py | 3 ++-
5 files changed, 25 insertions(+), 19 deletions(-)
diff --git a/airflow/utils/timezone.py b/airflow/utils/timezone.py
index f7116cd81f..12c75bef59 100644
--- a/airflow/utils/timezone.py
+++ b/airflow/utils/timezone.py
@@ -194,16 +194,17 @@ def datetime(*args, **kwargs):
return dt.datetime(*args, **kwargs)
-def parse(string: str, timezone=None) -> DateTime:
+def parse(string: str, timezone=None, *, strict=False) -> DateTime:
"""
Parse a time string and return an aware datetime.
:param string: time string
:param timezone: the timezone
+ :param strict: if False, it will fall back on the dateutil parser if unable to parse with pendulum
"""
from airflow.settings import TIMEZONE
- return pendulum.parse(string, tz=timezone or TIMEZONE, strict=False) # type: ignore
+ return pendulum.parse(string, tz=timezone or TIMEZONE, strict=strict) # type: ignore
@overload
diff --git a/airflow/www/views.py b/airflow/www/views.py
index b08158ea08..05a88115dd 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -265,17 +265,18 @@ def get_date_time_num_runs_dag_runs_form_data(www_request, session, dag):
}
-def _safe_parse_datetime(v, allow_empty=False) -> datetime.datetime | None:
+def _safe_parse_datetime(v, *, allow_empty=False, strict=True) -> datetime.datetime | None:
"""
Parse datetime and return error message for invalid dates.
:param v: the string value to be parsed
:param allow_empty: Set True to return none if empty str or None
+ :param strict: if False, it will fall back on the dateutil parser if unable to parse with pendulum
"""
if allow_empty is True and not v:
return None
try:
- return timezone.parse(v)
+ return timezone.parse(v, strict=strict)
except (TypeError, ParserError):
abort(400, f"Invalid datetime: {v!r}")
@@ -1638,7 +1639,7 @@ class Airflow(AirflowBaseView):
# Convert string datetime into actual datetime
try:
- execution_date = timezone.parse(execution_date_str)
+ execution_date = timezone.parse(execution_date_str, strict=True)
except ValueError:
error_message = (
f"Given execution date, {execution_date}, could not be identified as a date. "
@@ -2111,7 +2112,7 @@ class Airflow(AirflowBaseView):
)
try:
- execution_date = timezone.parse(request_execution_date)
+ execution_date = timezone.parse(request_execution_date, strict=True)
except ParserError:
flash("Invalid execution date", "error")
form = DateTimeForm(data={"execution_date": timezone.utcnow().isoformat()})
@@ -3707,7 +3708,7 @@ class Airflow(AirflowBaseView):
num_runs = conf.getint("webserver", "default_dag_run_display_number")
try:
- base_date = timezone.parse(request.args["base_date"])
+ base_date = timezone.parse(request.args["base_date"], strict=True)
except (KeyError, ValueError):
base_date = dag.get_latest_execution_date() or timezone.utcnow()
diff --git a/tests/www/views/test_views_extra_links.py b/tests/www/views/test_views_extra_links.py
index 9b6ec14d35..acd1dcee70 100644
--- a/tests/www/views/test_views_extra_links.py
+++ b/tests/www/views/test_views_extra_links.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import json
+import urllib.parse
from unittest import mock
import pytest
@@ -30,7 +31,8 @@ from airflow.utils.types import DagRunType
from tests.test_utils.db import clear_db_runs
from tests.test_utils.mock_operators import AirflowLink, Dummy2TestOperator, Dummy3TestOperator
-DEFAULT_DATE = timezone.datetime(2017, 1, 1)
+DEFAULT_DATE = timezone.datetime(2017, 1, 1, tzinfo=timezone.utc)
+STR_DEFAULT_DATE = urllib.parse.quote(DEFAULT_DATE.strftime("%Y-%m-%dT%H:%M:%S.%f%z"))
ENDPOINT = "extra_links"
@@ -129,7 +131,7 @@ def reset_task_instances():
def test_extra_links_works(dag_run, task_1, viewer_client, session):
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_1.dag_id}&task_id={task_1.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=foo-bar",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=foo-bar",
follow_redirects=True,
)
@@ -143,7 +145,7 @@ def test_extra_links_works(dag_run, task_1, viewer_client, session):
def test_global_extra_links_works(dag_run, task_1, viewer_client, session):
response = viewer_client.get(
f"{ENDPOINT}?dag_id={dag_run.dag_id}&task_id={task_1.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=github",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=github",
follow_redirects=True,
)
@@ -157,7 +159,7 @@ def test_global_extra_links_works(dag_run, task_1, viewer_client, session):
def test_operator_extra_link_override_global_extra_link(dag_run, task_1, viewer_client):
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_1.dag_id}&task_id={task_1.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=airflow",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=airflow",
follow_redirects=True,
)
@@ -171,7 +173,7 @@ def test_operator_extra_link_override_global_extra_link(dag_run, task_1, viewer_
def test_extra_links_error_raised(dag_run, task_1, viewer_client):
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_1.dag_id}&task_id={task_1.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=raise_error",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=raise_error",
follow_redirects=True,
)
@@ -185,7 +187,7 @@ def test_extra_links_error_raised(dag_run, task_1, viewer_client):
def test_extra_links_no_response(dag_run, task_1, viewer_client):
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_1.dag_id}&task_id={task_1.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=no_response",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=no_response",
follow_redirects=True,
)
@@ -206,7 +208,7 @@ def test_operator_extra_link_override_plugin(dag_run, task_2, viewer_client):
"""
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_2.dag_id}&task_id={task_2.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=airflow",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=airflow",
follow_redirects=True,
)
@@ -228,7 +230,7 @@ def test_operator_extra_link_multiple_operators(dag_run, task_2, task_3, viewer_
"""
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_2.dag_id}&task_id={task_2.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=airflow",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=airflow",
follow_redirects=True,
)
@@ -240,7 +242,7 @@ def test_operator_extra_link_multiple_operators(dag_run, task_2, task_3, viewer_
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_3.dag_id}&task_id={task_3.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=airflow",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=airflow",
follow_redirects=True,
)
@@ -253,7 +255,7 @@ def test_operator_extra_link_multiple_operators(dag_run, task_2, task_3, viewer_
# Also check that the other Operator Link defined for this operator exists
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_3.dag_id}&task_id={task_3.task_id}"
- f"&execution_date={DEFAULT_DATE}&link_name=google",
+ f"&execution_date={STR_DEFAULT_DATE}&link_name=google",
follow_redirects=True,
)
diff --git a/tests/www/views/test_views_log.py b/tests/www/views/test_views_log.py
index 207e9a3d89..aa8b583546 100644
--- a/tests/www/views/test_views_log.py
+++ b/tests/www/views/test_views_log.py
@@ -49,7 +49,8 @@ DAG_ID = "dag_for_testing_log_view"
DAG_ID_REMOVED = "removed_dag_for_testing_log_view"
TASK_ID = "task_for_testing_log_view"
DEFAULT_DATE = timezone.datetime(2017, 9, 1)
-ENDPOINT = f"log?dag_id={DAG_ID}&task_id={TASK_ID}&execution_date={DEFAULT_DATE}"
+STR_DEFAULT_DATE = urllib.parse.quote(DEFAULT_DATE.strftime("%Y-%m-%dT%H:%M:%S.%f%z"))
+ENDPOINT = f"log?dag_id={DAG_ID}&task_id={TASK_ID}&execution_date={STR_DEFAULT_DATE}"
@pytest.fixture(scope="module", autouse=True)
diff --git a/tests/www/views/test_views_tasks.py b/tests/www/views/test_views_tasks.py
index c7f10d2747..01fdc820ae 100644
--- a/tests/www/views/test_views_tasks.py
+++ b/tests/www/views/test_views_tasks.py
@@ -46,6 +46,7 @@ from tests.test_utils.db import clear_db_runs, clear_db_xcom
from tests.test_utils.www import check_content_in_response, check_content_not_in_response, client_with_login
DEFAULT_DATE = timezone.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
+STR_DEFAULT_DATE = urllib.parse.quote(DEFAULT_DATE.strftime("%Y-%m-%dT%H:%M:%S.%f%z"))
DEFAULT_VAL = urllib.parse.quote_plus(str(DEFAULT_DATE))
@@ -1041,7 +1042,7 @@ def test_graph_view_doesnt_fail_on_recursion_error(app, dag_maker, admin_client)
def test_task_instances(admin_client):
"""Test task_instances view."""
resp = admin_client.get(
- f"/object/task_instances?dag_id=example_bash_operator&execution_date={DEFAULT_DATE}",
+ f"/object/task_instances?dag_id=example_bash_operator&execution_date={STR_DEFAULT_DATE}",
follow_redirects=True,
)
assert resp.status_code == 200