This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun 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 e009e5302f6 AIP-84 Add user id when updating notes in TI (#48582)
e009e5302f6 is described below

commit e009e5302f607806c996d00467b2723cfc4118e4
Author: Kalyan R <[email protected]>
AuthorDate: Tue Apr 1 21:01:22 2025 +0530

    AIP-84 Add user id when updating notes in TI (#48582)
    
    * add user id to ti note
    
    * refresh ti_note to fix test
    
    * fix failing test
    
    * add tests to patch dry run
    
    * add explanation for adding session.refresh
---
 .../core_api/routes/public/task_instances.py       |  9 ++---
 .../core_api/routes/public/test_task_instances.py  | 44 ++++++++++++++++++----
 .../src/tests_common/test_utils/api_fastapi.py     | 14 +++++++
 3 files changed, 54 insertions(+), 13 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
index 5defd2b08ff..6a3afa74b4f 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
@@ -61,7 +61,7 @@ from airflow.api_fastapi.core_api.datamodels.task_instances 
import (
     TaskInstancesBatchBody,
 )
 from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
-from airflow.api_fastapi.core_api.security import ReadableTIFilterDep, 
requires_access_dag
+from airflow.api_fastapi.core_api.security import GetUserDep, 
ReadableTIFilterDep, requires_access_dag
 from airflow.api_fastapi.logging.decorators import action_logging
 from airflow.exceptions import TaskNotFound
 from airflow.models import Base, DagRun
@@ -853,6 +853,7 @@ def patch_task_instance(
     task_id: str,
     request: Request,
     body: PatchTaskInstanceBody,
+    user: GetUserDep,
     session: SessionDep,
     map_index: int = -1,
     update_mask: list[str] | None = Query(None),
@@ -883,13 +884,11 @@ def patch_task_instance(
             ti = tis[0] if isinstance(tis, list) else tis
         elif key == "note":
             if update_mask or body.note is not None:
-                # @TODO: replace None passed for user_id with actual user id 
when
-                # permissions and auth is in place.
                 if ti.task_instance_note is None:
-                    ti.note = (body.note, None)
+                    ti.note = (body.note, user.get_id())
                 else:
                     ti.task_instance_note.content = body.note
-                    ti.task_instance_note.user_id = None
+                    ti.task_instance_note.user_id = user.get_id()
                 session.commit()
 
     return TaskInstanceResponse.model_validate(ti)
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
index 486c6eaa8a7..228b6aa7f9f 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
@@ -42,6 +42,7 @@ from airflow.utils.state import DagRunState, State, 
TaskInstanceState
 from airflow.utils.timezone import datetime
 from airflow.utils.types import DagRunType
 
+from tests_common.test_utils.api_fastapi import _check_task_instance_note
 from tests_common.test_utils.db import (
     clear_db_runs,
     clear_rendered_ti_fields,
@@ -3355,13 +3356,21 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
         assert mock_set_ti_state.call_count == set_ti_state_call_count
 
     @pytest.mark.parametrize(
-        "new_note_value",
+        "new_note_value,ti_note_data",
         [
-            "My super cool TaskInstance note.",
-            None,
+            (
+                "My super cool TaskInstance note.",
+                {"content": "My super cool TaskInstance note.", "user_id": 
"test"},
+            ),
+            (
+                None,
+                {"content": None, "user_id": "test"},
+            ),
         ],
     )
-    def test_update_mask_set_note_should_respond_200(self, test_client, 
session, new_note_value):
+    def test_update_mask_set_note_should_respond_200(
+        self, test_client, session, new_note_value, ti_note_data
+    ):
         self.create_task_instances(session)
         response = test_client.patch(
             
"/dags/example_python_operator/dagRuns/TEST_DAG_RUN_ID/taskInstances/print_the_context",
@@ -3369,7 +3378,8 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
             json={"note": new_note_value},
         )
         assert response.status_code == 200, response.text
-        assert response.json() == {
+        response_data = response.json()
+        assert response_data == {
             "dag_id": self.DAG_ID,
             "dag_version": None,
             "duration": 10000.0,
@@ -3403,6 +3413,7 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
             "trigger": None,
             "triggerer_job": None,
         }
+        _check_task_instance_note(session, response_data["id"], ti_note_data)
 
     def test_set_note_should_respond_200(self, test_client, session):
         self.create_task_instances(session)
@@ -3412,7 +3423,8 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
             json={"note": new_note_value},
         )
         assert response.status_code == 200, response.text
-        assert response.json() == {
+        response_data = response.json()
+        assert response_data == {
             "dag_id": self.DAG_ID,
             "dag_version": None,
             "duration": 10000.0,
@@ -3447,6 +3459,10 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
             "triggerer_job": None,
         }
 
+        _check_task_instance_note(
+            session, response_data["id"], {"content": new_note_value, 
"user_id": "test"}
+        )
+
     def test_set_note_should_respond_200_mapped_task_instance_with_rtif(self, 
test_client, session):
         """Verify we don't duplicate rows through join to RTIF"""
         tis = self.create_task_instances(session)
@@ -3468,8 +3484,9 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
                 json={"note": new_note_value},
             )
             assert response.status_code == 200, response.text
+            response_data = response.json()
 
-            assert response.json() == {
+            assert response_data == {
                 "dag_id": self.DAG_ID,
                 "dag_version": None,
                 "duration": 10000.0,
@@ -3504,6 +3521,10 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
                 "triggerer_job": None,
             }
 
+            _check_task_instance_note(
+                session, response_data["id"], {"content": new_note_value, 
"user_id": "test"}
+            )
+
     def test_set_note_should_respond_200_when_note_is_empty(self, test_client, 
session):
         tis = self.create_task_instances(session)
         for ti in tis:
@@ -3516,7 +3537,11 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
             json={"note": new_note_value},
         )
         assert response.status_code == 200, response.text
-        assert response.json()["note"] == new_note_value
+        response_data = response.json()
+        assert response_data["note"] == new_note_value
+        _check_task_instance_note(
+            session, response_data["id"], {"content": new_note_value, 
"user_id": "test"}
+        )
 
     @mock.patch("airflow.models.dag.DAG.set_task_instance_state")
     def test_should_raise_409_for_updating_same_task_instance_state(
@@ -3650,6 +3675,8 @@ class 
TestPatchTaskInstanceDryRun(TestTaskInstanceEndpoint):
 
         assert task_before == task_after
 
+        _check_task_instance_note(session, task_after["id"], {"content": 
"placeholder-note", "user_id": None})
+
     def test_should_respond_401(self, unauthenticated_test_client):
         response = unauthenticated_test_client.patch(
             f"{self.ENDPOINT_URL}/dry_run",
@@ -3687,6 +3714,7 @@ class 
TestPatchTaskInstanceDryRun(TestTaskInstanceEndpoint):
         task_after = test_client.get(f"{self.ENDPOINT_URL}/{map_index}").json()
 
         assert task_before == task_after
+        _check_task_instance_note(session, task_after["id"], None)
 
     @pytest.mark.parametrize(
         "error, code, payload",
diff --git a/devel-common/src/tests_common/test_utils/api_fastapi.py 
b/devel-common/src/tests_common/test_utils/api_fastapi.py
index 5c99ceeb9ee..10399f919ca 100644
--- a/devel-common/src/tests_common/test_utils/api_fastapi.py
+++ b/devel-common/src/tests_common/test_utils/api_fastapi.py
@@ -20,6 +20,7 @@ import json
 
 from airflow.models import DagRun, Log
 from airflow.models.dagrun import DagRunNote
+from airflow.models.taskinstance import TaskInstanceNote
 from airflow.sdk.execution_time.secrets_masker import DEFAULT_SENSITIVE_FIELDS 
as sensitive_fields
 
 
@@ -81,3 +82,16 @@ def _check_dag_run_note(session, dr_id, note_data):
     else:
         assert dr_note.user_id == note_data.get("user_id")
         assert dr_note.content == note_data.get("content")
+
+
+def _check_task_instance_note(session, ti_id, note_data):
+    ti_note = 
session.query(TaskInstanceNote).filter_by(ti_id=ti_id).one_or_none()
+    if note_data is None:
+        assert ti_note is None
+    else:
+        # Had to add this refresh because 
TestPatchTaskInstance::test_set_note_should_respond_200_mapped_task_instance_with_rtif
+        # was failing for map index = 2. Unless I force refresh ti_note, it 
was returning the old value.
+        # Even if I reverse the order of map indexes, only the map index 2 was 
returning older value.
+        session.refresh(ti_note)
+        assert ti_note.content == note_data["content"]
+        assert ti_note.user_id == note_data["user_id"]

Reply via email to