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