This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 7fef4a28399936426760594167fc38bd538533fc Author: Wei Lee <[email protected]> AuthorDate: Fri Sep 12 03:10:52 2025 +0800 fix(hitl): make the user model in HITLDetail consistent with airflow user model (#55463) (cherry picked from commit 30c193b103cf9f48038a5898e72d29b1ad8b3dcc) --- airflow-core/docs/img/airflow_erd.sha256 | 2 +- airflow-core/docs/img/airflow_erd.svg | 120 ++++++++++----------- airflow-core/docs/tutorial/hitl.rst | 3 + .../src/airflow/api_fastapi/common/parameters.py | 9 +- .../api_fastapi/core_api/datamodels/hitl.py | 15 ++- .../api_fastapi/core_api/openapi/_private_ui.yaml | 36 ++++--- .../core_api/openapi/v2-rest-api-generated.yaml | 55 +++++----- .../api_fastapi/core_api/routes/public/hitl.py | 16 +-- .../api_fastapi/execution_api/datamodels/hitl.py | 24 ++++- .../api_fastapi/execution_api/routes/hitl.py | 5 +- .../0076_3_1_0_add_human_in_the_loop_response.py | 5 +- airflow-core/src/airflow/models/hitl.py | 101 ++++++++++++++++- .../src/airflow/ui/openapi-gen/queries/common.ts | 8 +- .../ui/openapi-gen/queries/ensureQueryData.ts | 12 +-- .../src/airflow/ui/openapi-gen/queries/prefetch.ts | 12 +-- .../src/airflow/ui/openapi-gen/queries/queries.ts | 12 +-- .../src/airflow/ui/openapi-gen/queries/suspense.ts | 12 +-- .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 65 +++++------ .../ui/openapi-gen/requests/services.gen.ts | 8 +- .../airflow/ui/openapi-gen/requests/types.gen.ts | 20 ++-- .../core_api/routes/public/test_hitl.py | 30 +++--- .../api_fastapi/core_api/routes/ui/test_dags.py | 3 +- .../execution_api/versions/head/test_hitl.py | 22 ++-- .../src/airflowctl/api/datamodels/generated.py | 27 +++-- .../standard/example_dags/example_hitl_operator.py | 1 + .../airflow/providers/standard/operators/hitl.py | 8 +- .../airflow/providers/standard/triggers/hitl.py | 31 ++++-- .../tests/unit/standard/operators/test_hitl.py | 57 +++++++--- .../tests/unit/standard/triggers/test_hitl.py | 26 +++-- task-sdk/src/airflow/sdk/api/client.py | 5 +- .../src/airflow/sdk/api/datamodels/_generated.py | 54 ++++++---- task-sdk/src/airflow/sdk/execution_time/hitl.py | 16 ++- .../src/airflow/sdk/execution_time/supervisor.py | 2 +- task-sdk/tests/task_sdk/api/test_client.py | 15 ++- .../tests/task_sdk/execution_time/test_hitl.py | 19 ++-- .../task_sdk/execution_time/test_supervisor.py | 4 +- 36 files changed, 529 insertions(+), 331 deletions(-) diff --git a/airflow-core/docs/img/airflow_erd.sha256 b/airflow-core/docs/img/airflow_erd.sha256 index ee840ec21f2..b6b5a4a9270 100644 --- a/airflow-core/docs/img/airflow_erd.sha256 +++ b/airflow-core/docs/img/airflow_erd.sha256 @@ -1 +1 @@ -c05cefbe080ed889ebe132a0285756db477ff28256bbc1e86da1e053873f6478 \ No newline at end of file +e491b0c58188f06ab4696cc09c765413065069f90d78e27eb53fdac5e2e92c82 \ No newline at end of file diff --git a/airflow-core/docs/img/airflow_erd.svg b/airflow-core/docs/img/airflow_erd.svg index a129862ad11..f08f88ff3f6 100644 --- a/airflow-core/docs/img/airflow_erd.svg +++ b/airflow-core/docs/img/airflow_erd.svg @@ -1437,72 +1437,68 @@ <!-- hitl_detail --> <g id="node43" class="node"> <title>hitl_detail</title> -<polygon fill="none" stroke="black" points="2170,-2328 2170,-2356 2463,-2356 2463,-2328 2170,-2328"/> -<text text-anchor="start" x="2272.5" y="-2339.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">hitl_detail</text> -<polygon fill="none" stroke="black" points="2170,-2303 2170,-2328 2463,-2328 2463,-2303 2170,-2303"/> -<text text-anchor="start" x="2175" y="-2312.8" font-family="Helvetica,sans-Serif" text-decoration="underline" font-size="14.00">ti_id</text> -<text text-anchor="start" x="2204" y="-2312.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2209" y="-2312.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [UUID]</text> -<text text-anchor="start" x="2261" y="-2312.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2170,-2278 2170,-2303 2463,-2303 2463,-2278 2170,-2278"/> -<text text-anchor="start" x="2175" y="-2287.8" font-family="Helvetica,sans-Serif" font-size="14.00">body</text> -<text text-anchor="start" x="2210" y="-2287.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2215" y="-2287.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> -<polygon fill="none" stroke="black" points="2170,-2253 2170,-2278 2463,-2278 2463,-2253 2170,-2253"/> -<text text-anchor="start" x="2175" y="-2262.8" font-family="Helvetica,sans-Serif" font-size="14.00">chosen_options</text> -<text text-anchor="start" x="2283" y="-2262.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2288" y="-2262.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2170,-2228 2170,-2253 2463,-2253 2463,-2228 2170,-2228"/> -<text text-anchor="start" x="2175" y="-2237.8" font-family="Helvetica,sans-Serif" font-size="14.00">defaults</text> -<text text-anchor="start" x="2232" y="-2237.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2237" y="-2237.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2170,-2203 2170,-2228 2463,-2228 2463,-2203 2170,-2203"/> -<text text-anchor="start" x="2175" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00">multiple</text> -<text text-anchor="start" x="2232" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2237" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [BOOLEAN]</text> -<polygon fill="none" stroke="black" points="2170,-2178 2170,-2203 2463,-2203 2463,-2178 2170,-2178"/> -<text text-anchor="start" x="2175" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00">options</text> -<text text-anchor="start" x="2227" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2232" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<text text-anchor="start" x="2283" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2170,-2153 2170,-2178 2463,-2178 2463,-2153 2170,-2153"/> -<text text-anchor="start" x="2175" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00">params</text> -<text text-anchor="start" x="2228" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2233" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<text text-anchor="start" x="2284" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2170,-2128 2170,-2153 2463,-2153 2463,-2128 2170,-2128"/> -<text text-anchor="start" x="2175" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00">params_input</text> -<text text-anchor="start" x="2271" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2276" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<text text-anchor="start" x="2327" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2170,-2103 2170,-2128 2463,-2128 2463,-2103 2170,-2103"/> -<text text-anchor="start" x="2175" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_user_id</text> -<text text-anchor="start" x="2306" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2311" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [VARCHAR(128)]</text> -<polygon fill="none" stroke="black" points="2170,-2078 2170,-2103 2463,-2103 2463,-2078 2170,-2078"/> -<text text-anchor="start" x="2175" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_user_name</text> -<text text-anchor="start" x="2332" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2337" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [VARCHAR(128)]</text> -<polygon fill="none" stroke="black" points="2170,-2053 2170,-2078 2463,-2078 2463,-2053 2170,-2053"/> -<text text-anchor="start" x="2175" y="-2062.8" font-family="Helvetica,sans-Serif" font-size="14.00">respondents</text> -<text text-anchor="start" x="2262" y="-2062.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2267" y="-2062.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2170,-2028 2170,-2053 2463,-2053 2463,-2028 2170,-2028"/> -<text text-anchor="start" x="2175" y="-2037.8" font-family="Helvetica,sans-Serif" font-size="14.00">response_at</text> -<text text-anchor="start" x="2259" y="-2037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2264" y="-2037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TIMESTAMP]</text> -<polygon fill="none" stroke="black" points="2170,-2003 2170,-2028 2463,-2028 2463,-2003 2170,-2003"/> -<text text-anchor="start" x="2175" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00">subject</text> -<text text-anchor="start" x="2226" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2231" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> -<text text-anchor="start" x="2281" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2197,-2304 2197,-2332 2435,-2332 2435,-2304 2197,-2304"/> +<text text-anchor="start" x="2272" y="-2315.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">hitl_detail</text> +<polygon fill="none" stroke="black" points="2197,-2279 2197,-2304 2435,-2304 2435,-2279 2197,-2279"/> +<text text-anchor="start" x="2202" y="-2288.8" font-family="Helvetica,sans-Serif" text-decoration="underline" font-size="14.00">ti_id</text> +<text text-anchor="start" x="2231" y="-2288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2236" y="-2288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [UUID]</text> +<text text-anchor="start" x="2288" y="-2288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2197,-2254 2197,-2279 2435,-2279 2435,-2254 2197,-2254"/> +<text text-anchor="start" x="2202" y="-2263.8" font-family="Helvetica,sans-Serif" font-size="14.00">assignees</text> +<text text-anchor="start" x="2272" y="-2263.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2277" y="-2263.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2197,-2229 2197,-2254 2435,-2254 2435,-2229 2197,-2229"/> +<text text-anchor="start" x="2202" y="-2238.8" font-family="Helvetica,sans-Serif" font-size="14.00">body</text> +<text text-anchor="start" x="2237" y="-2238.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2242" y="-2238.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> +<polygon fill="none" stroke="black" points="2197,-2204 2197,-2229 2435,-2229 2435,-2204 2197,-2204"/> +<text text-anchor="start" x="2202" y="-2213.8" font-family="Helvetica,sans-Serif" font-size="14.00">chosen_options</text> +<text text-anchor="start" x="2310" y="-2213.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2315" y="-2213.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2197,-2179 2197,-2204 2435,-2204 2435,-2179 2197,-2179"/> +<text text-anchor="start" x="2202" y="-2188.8" font-family="Helvetica,sans-Serif" font-size="14.00">defaults</text> +<text text-anchor="start" x="2259" y="-2188.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2264" y="-2188.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2197,-2154 2197,-2179 2435,-2179 2435,-2154 2197,-2154"/> +<text text-anchor="start" x="2202" y="-2163.8" font-family="Helvetica,sans-Serif" font-size="14.00">multiple</text> +<text text-anchor="start" x="2259" y="-2163.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2264" y="-2163.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [BOOLEAN]</text> +<polygon fill="none" stroke="black" points="2197,-2129 2197,-2154 2435,-2154 2435,-2129 2197,-2129"/> +<text text-anchor="start" x="2202" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00">options</text> +<text text-anchor="start" x="2254" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2259" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<text text-anchor="start" x="2310" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2197,-2104 2197,-2129 2435,-2129 2435,-2104 2197,-2104"/> +<text text-anchor="start" x="2202" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00">params</text> +<text text-anchor="start" x="2255" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2260" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<text text-anchor="start" x="2311" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2197,-2079 2197,-2104 2435,-2104 2435,-2079 2197,-2079"/> +<text text-anchor="start" x="2202" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00">params_input</text> +<text text-anchor="start" x="2298" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2303" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<text text-anchor="start" x="2354" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2197,-2054 2197,-2079 2435,-2079 2435,-2054 2197,-2054"/> +<text text-anchor="start" x="2202" y="-2063.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_by</text> +<text text-anchor="start" x="2300" y="-2063.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2305" y="-2063.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2197,-2029 2197,-2054 2435,-2054 2435,-2029 2197,-2029"/> +<text text-anchor="start" x="2202" y="-2038.8" font-family="Helvetica,sans-Serif" font-size="14.00">response_at</text> +<text text-anchor="start" x="2286" y="-2038.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2291" y="-2038.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TIMESTAMP]</text> +<polygon fill="none" stroke="black" points="2197,-2004 2197,-2029 2435,-2029 2435,-2004 2197,-2004"/> +<text text-anchor="start" x="2202" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00">subject</text> +<text text-anchor="start" x="2253" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2258" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> +<text text-anchor="start" x="2308" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> </g> <!-- task_instance--hitl_detail --> <g id="edge48" class="edge"> <title>task_instance--hitl_detail</title> -<path fill="none" stroke="#7f7f7f" stroke-dasharray="5,2" d="M2081.12,-1859.57C2103.71,-1904.92 2128.13,-1949.64 2154,-1991 2156.4,-1994.84 2158.87,-1998.68 2161.41,-2002.51"/> -<text text-anchor="start" x="2151.41" y="-1991.31" font-family="Times,serif" font-size="14.00">1</text> -<text text-anchor="start" x="2081.12" y="-1848.37" font-family="Times,serif" font-size="14.00">1</text> +<path fill="none" stroke="#7f7f7f" stroke-dasharray="5,2" d="M2081.05,-1862.88C2103.53,-1907.23 2127.94,-1950.81 2154,-1991 2164.45,-2007.12 2176.38,-2023.19 2188.91,-2038.71"/> +<text text-anchor="start" x="2178.91" y="-2027.51" font-family="Times,serif" font-size="14.00">1</text> +<text text-anchor="start" x="2081.05" y="-1851.68" font-family="Times,serif" font-size="14.00">1</text> </g> <!-- task_map --> <g id="node44" class="node"> diff --git a/airflow-core/docs/tutorial/hitl.rst b/airflow-core/docs/tutorial/hitl.rst index b104bc7d033..a84ef735598 100644 --- a/airflow-core/docs/tutorial/hitl.rst +++ b/airflow-core/docs/tutorial/hitl.rst @@ -95,6 +95,9 @@ Approval or Rejection --------------------- A specialized form of option selection, which has only 'Approval' and 'Rejection' as options. +You can also set the ``assigned_users`` to restrict the users allowed to respond for a HITL operator. +It should be a list of user ids and user names (both needed) (e.g., ``[{"id": "1", "name": "user1"}, {"id": "2", "name": "user2"}]``. +ONLY the users within this list will be allowed to respond. .. exampleinclude:: /../../providers/standard/src/airflow/providers/standard/example_dags/example_hitl_operator.py :language: python diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index 7a555454cd1..d2dce9743f7 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -1025,15 +1025,16 @@ QueryHITLDetailResponseReceivedFilter = Annotated[ ) ), ] + QueryHITLDetailRespondedUserIdFilter = Annotated[ FilterParam[list[str]], Depends( filter_param_factory( - HITLDetail.responded_user_id, + HITLDetail.responded_by_user_id, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list, - filter_name="responded_user_id", + filter_name="responded_by_user_id", ) ), ] @@ -1041,11 +1042,11 @@ QueryHITLDetailRespondedUserNameFilter = Annotated[ FilterParam[list[str]], Depends( filter_param_factory( - HITLDetail.responded_user_name, + HITLDetail.responded_by_user_name, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list, - filter_name="responded_user_name", + filter_name="responded_by_user_name", ) ), ] diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py index ea0f045539e..24a18821f13 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py @@ -37,13 +37,19 @@ class UpdateHITLDetailPayload(BaseModel): class HITLDetailResponse(BaseModel): """Response of updating a Human-in-the-loop detail.""" - responded_user_id: str - responded_user_name: str + responded_by: HITLUser response_at: datetime chosen_options: list[str] = Field(min_length=1) params_input: Mapping = Field(default_factory=dict) +class HITLUser(BaseModel): + """Schema for a Human-in-the-loop users.""" + + id: str + name: str + + class HITLDetail(BaseModel): """Schema for Human-in-the-loop detail.""" @@ -56,11 +62,10 @@ class HITLDetail(BaseModel): defaults: list[str] | None = None multiple: bool = False params: dict[str, Any] = Field(default_factory=dict) - respondents: list[str] | None = None + assigned_users: list[HITLUser] = Field(default_factory=list) # Response Content Detail - responded_user_id: str | None = None - responded_user_name: str | None = None + responded_by_user: HITLUser | None = None response_at: datetime | None = None chosen_options: list[str] | None = None params_input: dict[str, Any] = Field(default_factory=dict) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index a4d85b16dd3..383b9fca62f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -1994,23 +1994,15 @@ components: additionalProperties: true type: object title: Params - respondents: - anyOf: - - items: - type: string - type: array - - type: 'null' - title: Respondents - responded_user_id: - anyOf: - - type: string - - type: 'null' - title: Responded User Id - responded_user_name: + assigned_users: + items: + $ref: '#/components/schemas/HITLUser' + type: array + title: Assigned Users + responded_by_user: anyOf: - - type: string + - $ref: '#/components/schemas/HITLUser' - type: 'null' - title: Responded User Name response_at: anyOf: - type: string @@ -2039,6 +2031,20 @@ components: - subject title: HITLDetail description: Schema for Human-in-the-loop detail. + HITLUser: + properties: + id: + type: string + title: Id + name: + type: string + title: Name + type: object + required: + - id + - name + title: HITLUser + description: Schema for a Human-in-the-loop users. HTTPExceptionResponse: properties: detail: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index f9057f2d5ca..9150f12c012 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -8200,22 +8200,22 @@ paths: - type: boolean - type: 'null' title: Response Received - - name: responded_user_id + - name: responded_by_user_id in: query required: false schema: type: array items: type: string - title: Responded User Id - - name: responded_user_name + title: Responded By User Id + - name: responded_by_user_name in: query required: false schema: type: array items: type: string - title: Responded User Name + title: Responded By User Name - name: subject_search in: query required: false @@ -10856,23 +10856,15 @@ components: additionalProperties: true type: object title: Params - respondents: - anyOf: - - items: - type: string - type: array - - type: 'null' - title: Respondents - responded_user_id: - anyOf: - - type: string - - type: 'null' - title: Responded User Id - responded_user_name: + assigned_users: + items: + $ref: '#/components/schemas/HITLUser' + type: array + title: Assigned Users + responded_by_user: anyOf: - - type: string + - $ref: '#/components/schemas/HITLUser' - type: 'null' - title: Responded User Name response_at: anyOf: - type: string @@ -10919,12 +10911,8 @@ components: description: Schema for a collection of Human-in-the-loop details. HITLDetailResponse: properties: - responded_user_id: - type: string - title: Responded User Id - responded_user_name: - type: string - title: Responded User Name + responded_by: + $ref: '#/components/schemas/HITLUser' response_at: type: string format: date-time @@ -10941,12 +10929,25 @@ components: title: Params Input type: object required: - - responded_user_id - - responded_user_name + - responded_by - response_at - chosen_options title: HITLDetailResponse description: Response of updating a Human-in-the-loop detail. + HITLUser: + properties: + id: + type: string + title: Id + name: + type: string + title: Name + type: object + required: + - id + - name + title: HITLUser + description: Schema for a Human-in-the-loop users. HTTPExceptionResponse: properties: detail: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py index e04714e0a91..5f39e18dd13 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py @@ -53,7 +53,7 @@ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_ from airflow.api_fastapi.core_api.security import GetUserDep, ReadableTIFilterDep, requires_access_dag from airflow.api_fastapi.logging.decorators import action_logging from airflow.models.dagrun import DagRun -from airflow.models.hitl import HITLDetail as HITLDetailModel +from airflow.models.hitl import HITLDetail as HITLDetailModel, HITLUser from airflow.models.taskinstance import TaskInstance as TI hitl_router = AirflowRouter(tags=["HumanInTheLoop"], prefix="/hitlDetails") @@ -144,19 +144,19 @@ def update_hitl_detail( user_id = user.get_id() user_name = user.get_name() - if hitl_detail_model.respondents: - if isinstance(user_id, int): - # FabAuthManager (ab_user) store user id as integer, but common interface is string type - user_id = str(user_id) - if user_id not in hitl_detail_model.respondents: + if isinstance(user_id, int): + # FabAuthManager (ab_user) store user id as integer, but common interface is string type + user_id = str(user_id) + hitl_user = HITLUser(id=user_id, name=user_name) + if hitl_detail_model.assigned_users: + if hitl_user not in hitl_detail_model.assigned_users: log.error("User=%s (id=%s) is not a respondent for the task", user_name, user_id) raise HTTPException( status.HTTP_403_FORBIDDEN, f"User={user_name} (id={user_id}) is not a respondent for the task.", ) - hitl_detail_model.responded_user_id = user_id - hitl_detail_model.responded_user_name = user_name + hitl_detail_model.responded_by = hitl_user hitl_detail_model.response_at = timezone.utcnow() hitl_detail_model.chosen_options = update_hitl_detail_payload.chosen_options hitl_detail_model.params_input = update_hitl_detail_payload.params_input diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py index 4162d2cf771..a5f16eb8818 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py @@ -26,6 +26,13 @@ from airflow.api_fastapi.core_api.base import BaseModel from airflow.models.hitl import HITLDetail +class HITLUser(BaseModel): + """Schema for a Human-in-the-loop users.""" + + id: str + name: str + + class HITLDetailRequest(BaseModel): """Schema for the request part of a Human-in-the-loop detail for a specific task instance.""" @@ -36,7 +43,7 @@ class HITLDetailRequest(BaseModel): defaults: list[str] | None = None multiple: bool = False params: dict[str, Any] = Field(default_factory=dict) - respondents: list[str] | None = None + assigned_users: list[HITLUser] = Field(default_factory=list) class UpdateHITLDetailPayload(BaseModel): @@ -51,8 +58,7 @@ class HITLDetailResponse(BaseModel): """Schema for the response part of a Human-in-the-loop detail for a specific task instance.""" response_received: bool - responded_user_name: str | None - responded_user_id: str | None + responded_by_user: HITLUser | None = None response_at: datetime | None # It's empty if the user has not yet responded. chosen_options: list[str] | None @@ -60,11 +66,19 @@ class HITLDetailResponse(BaseModel): @classmethod def from_hitl_detail_orm(cls, hitl_detail: HITLDetail) -> HITLDetailResponse: + hitl_user = ( + HITLUser( + id=hitl_detail.responded_by_user_id, + name=hitl_detail.responded_by_user_name, + ) + if hitl_detail.responded_by_user + else None + ) + return HITLDetailResponse( response_received=hitl_detail.response_received, response_at=hitl_detail.response_at, - responded_user_id=hitl_detail.responded_user_id, - responded_user_name=hitl_detail.responded_user_name, + responded_by_user=hitl_user, chosen_options=hitl_detail.chosen_options, params_input=hitl_detail.params_input or {}, ) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py index c9efbda78f0..8a6606bd280 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py @@ -71,7 +71,7 @@ def upsert_hitl_detail( defaults=payload.defaults, multiple=payload.multiple, params=payload.params, - respondents=payload.respondents, + assignees=[user.model_dump() for user in payload.assigned_users], ) session.add(hitl_detail_model) elif hitl_detail_model.response_received: @@ -116,8 +116,7 @@ def update_hitl_detail( f"Human-in-the-loop detail for Task Instance with id {ti_id_str} already exists.", ) - hitl_detail_model.responded_user_id = HITLDetail.DEFAULT_USER_NAME - hitl_detail_model.responded_user_name = HITLDetail.DEFAULT_USER_NAME + hitl_detail_model.responded_by = HITLDetail.DEFAULT_USER hitl_detail_model.response_at = datetime.now(timezone.utc) hitl_detail_model.chosen_options = payload.chosen_options hitl_detail_model.params_input = payload.params_input diff --git a/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py b/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py index 3e0ede0183e..60f870ff44c 100644 --- a/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py +++ b/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py @@ -59,10 +59,9 @@ def upgrade(): Column("defaults", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("multiple", Boolean, unique=False, default=False), Column("params", sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}), - Column("respondents", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), + Column("assignees", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("response_at", UtcDateTime, nullable=True), - Column("responded_user_id", String(128), nullable=True), - Column("responded_user_name", String(128), nullable=True), + Column("responded_by", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("chosen_options", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("params_input", sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}), ForeignKeyConstraint( diff --git a/airflow-core/src/airflow/models/hitl.py b/airflow-core/src/airflow/models/hitl.py index 448939e2c6f..e2055344bc3 100644 --- a/airflow-core/src/airflow/models/hitl.py +++ b/airflow-core/src/airflow/models/hitl.py @@ -16,16 +16,68 @@ # under the License. from __future__ import annotations +from typing import TYPE_CHECKING, Any, TypedDict + import sqlalchemy_jsonfield -from sqlalchemy import Boolean, Column, ForeignKeyConstraint, String, Text +from sqlalchemy import Boolean, Column, ForeignKeyConstraint, String, Text, func, literal from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.compiler import compiles from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship +from sqlalchemy.sql.functions import FunctionElement from airflow.models.base import Base from airflow.settings import json from airflow.utils.sqlalchemy import UtcDateTime +if TYPE_CHECKING: + from sqlalchemy.sql import ColumnElement + from sqlalchemy.sql.compiler import SQLCompiler + + +class JSONExtract(FunctionElement): + """ + Cross-dialect JSON key extractor. + + :meta: private + """ + + type = String() + inherit_cache = True + + def __init__(self, column: ColumnElement[Any], key: str, **kwargs: dict[str, Any]) -> None: + super().__init__(column, literal(key), **kwargs) + + +@compiles(JSONExtract, "postgresql") +def compile_postgres(element: JSONExtract, compiler: SQLCompiler, **kwargs: dict[str, Any]) -> str: + """ + Compile JSONExtract for PostgreSQL. + + :meta: private + """ + column, key = element.clauses + return compiler.process(func.json_extract_path_text(column, key), **kwargs) + + +@compiles(JSONExtract, "sqlite") +@compiles(JSONExtract, "mysql") +def compile_sqlite_mysql(element: JSONExtract, compiler: SQLCompiler, **kwargs: dict[str, Any]) -> str: + """ + Compile JSONExtract for SQLite/MySQL. + + :meta: private + """ + column, key = element.clauses + return compiler.process(func.json_extract(column, f"$.{key.value}"), **kwargs) + + +class HITLUser(TypedDict): + """Typed dict for saving a Human-in-the-loop user information.""" + + id: str + name: str + class HITLDetail(Base): """Human-in-the-loop request and corresponding response.""" @@ -44,12 +96,11 @@ class HITLDetail(Base): defaults = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) multiple = Column(Boolean, unique=False, default=False) params = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}) - respondents = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) + assignees = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) # Response Content Detail response_at = Column(UtcDateTime, nullable=True) - responded_user_id = Column(String(128), nullable=True) - responded_user_name = Column(String(128), nullable=True) + responded_by = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) chosen_options = Column( sqlalchemy_jsonfield.JSONField(json=json), nullable=True, @@ -80,4 +131,44 @@ class HITLDetail(Base): def response_received(cls): return cls.response_at.is_not(None) - DEFAULT_USER_NAME = "Fallback to defaults" + @hybrid_property + def responded_by_user_id(self) -> str | None: + return self.responded_by["id"] if self.responded_by else None + + @responded_by_user_id.expression # type: ignore[no-redef] + def responded_by_user_id(cls): + return JSONExtract(cls.responded_by, "id") + + @hybrid_property + def responded_by_user_name(self) -> str | None: + return self.responded_by["name"] if self.responded_by else None + + @responded_by_user_name.expression # type: ignore[no-redef] + def responded_by_user_name(cls): + return JSONExtract(cls.responded_by, "name") + + @hybrid_property + def assigned_users(self) -> list[HITLUser]: + if not self.assignees: + return [] + return [ + HITLUser( + id=assignee["id"], + name=assignee["name"], + ) + for assignee in self.assignees + ] + + @hybrid_property + def responded_by_user(self) -> HITLUser | None: + if self.responded_by is None: + return None + return HITLUser( + id=self.responded_by["id"], + name=self.responded_by["name"], + ) + + DEFAULT_USER = HITLUser( + id="Fallback to defaults", + name="Fallback to defaults", + ) diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index 457aac893ee..d4ffa8098f9 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -717,7 +717,7 @@ export const UseHumanInTheLoopServiceGetHitlDetailKeyFn = ({ dagId, dagRunId, ma export type HumanInTheLoopServiceGetHitlDetailsDefaultResponse = Awaited<ReturnType<typeof HumanInTheLoopService.getHitlDetails>>; export type HumanInTheLoopServiceGetHitlDetailsQueryResult<TData = HumanInTheLoopServiceGetHitlDetailsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useHumanInTheLoopServiceGetHitlDetailsKey = "HumanInTheLoopServiceGetHitlDetails"; -export const UseHumanInTheLoopServiceGetHitlDetailsKeyFn = ({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const UseHumanInTheLoopServiceGetHitlDetailsKeyFn = ({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; dagId?: string; dagIdPattern?: string; @@ -725,14 +725,14 @@ export const UseHumanInTheLoopServiceGetHitlDetailsKeyFn = ({ bodySearch, dagId, limit?: number; offset?: number; orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; responseReceived?: boolean; state?: string[]; subjectSearch?: string; taskId?: string; taskIdPattern?: string; -} = {}, queryKey?: Array<unknown>) => [useHumanInTheLoopServiceGetHitlDetailsKey, ...(queryKey ?? [{ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }])]; +} = {}, queryKey?: Array<unknown>) => [useHumanInTheLoopServiceGetHitlDetailsKey, ...(queryKey ?? [{ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }])]; export type MonitorServiceGetHealthDefaultResponse = Awaited<ReturnType<typeof MonitorService.getHealth>>; export type MonitorServiceGetHealthQueryResult<TData = MonitorServiceGetHealthDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useMonitorServiceGetHealthKey = "MonitorServiceGetHealth"; diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts index fac229938af..da76a35a8ce 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1371,14 +1371,14 @@ export const ensureUseHumanInTheLoopServiceGetHitlDetailData = (queryClient: Que * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.state * @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName +* @param data.respondedByUserId +* @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const ensureUseHumanInTheLoopServiceGetHitlDetailsData = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const ensureUseHumanInTheLoopServiceGetHitlDetailsData = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; dagId?: string; dagIdPattern?: string; @@ -1386,14 +1386,14 @@ export const ensureUseHumanInTheLoopServiceGetHitlDetailsData = (queryClient: Qu limit?: number; offset?: number; orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; responseReceived?: boolean; state?: string[]; subjectSearch?: string; taskId?: string; taskIdPattern?: string; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskId [...] /** * Get Health * @returns HealthInfoResponse Successful Response diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts index 1870bb85c32..af773c94bdd 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1371,14 +1371,14 @@ export const prefetchUseHumanInTheLoopServiceGetHitlDetail = (queryClient: Query * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.state * @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName +* @param data.respondedByUserId +* @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const prefetchUseHumanInTheLoopServiceGetHitlDetails = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const prefetchUseHumanInTheLoopServiceGetHitlDetails = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; dagId?: string; dagIdPattern?: string; @@ -1386,14 +1386,14 @@ export const prefetchUseHumanInTheLoopServiceGetHitlDetails = (queryClient: Quer limit?: number; offset?: number; orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; responseReceived?: boolean; state?: string[]; subjectSearch?: string; taskId?: string; taskIdPattern?: string; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPa [...] /** * Get Health * @returns HealthInfoResponse Successful Response diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts index e48235d6184..6c70c69ac5c 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1371,14 +1371,14 @@ export const useHumanInTheLoopServiceGetHitlDetail = <TData = Common.HumanInTheL * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.state * @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName +* @param data.respondedByUserId +* @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const useHumanInTheLoopServiceGetHitlDetails = <TData = Common.HumanInTheLoopServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const useHumanInTheLoopServiceGetHitlDetails = <TData = Common.HumanInTheLoopServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; dagId?: string; dagIdPattern?: string; @@ -1386,14 +1386,14 @@ export const useHumanInTheLoopServiceGetHitlDetails = <TData = Common.HumanInThe limit?: number; offset?: number; orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; responseReceived?: boolean; state?: string[]; subjectSearch?: string; taskId?: string; taskIdPattern?: string; -} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy [...] +} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, ord [...] /** * Get Health * @returns HealthInfoResponse Successful Response diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts index a8578766cd5..79a5f974e07 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -1371,14 +1371,14 @@ export const useHumanInTheLoopServiceGetHitlDetailSuspense = <TData = Common.Hum * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.state * @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName +* @param data.respondedByUserId +* @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const useHumanInTheLoopServiceGetHitlDetailsSuspense = <TData = Common.HumanInTheLoopServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const useHumanInTheLoopServiceGetHitlDetailsSuspense = <TData = Common.HumanInTheLoopServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; dagId?: string; dagIdPattern?: string; @@ -1386,14 +1386,14 @@ export const useHumanInTheLoopServiceGetHitlDetailsSuspense = <TData = Common.Hu limit?: number; offset?: number; orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; responseReceived?: boolean; state?: string[]; subjectSearch?: string; taskId?: string; taskIdPattern?: string; -} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, [...] +} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, off [...] /** * Get Health * @returns HealthInfoResponse Successful Response diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 72795ca4551..332d112cb7c 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -3571,41 +3571,22 @@ export const $HITLDetail = { type: 'object', title: 'Params' }, - respondents: { - anyOf: [ - { - items: { - type: 'string' - }, - type: 'array' - }, - { - type: 'null' - } - ], - title: 'Respondents' - }, - responded_user_id: { - anyOf: [ - { - type: 'string' - }, - { - type: 'null' - } - ], - title: 'Responded User Id' + assigned_users: { + items: { + '$ref': '#/components/schemas/HITLUser' + }, + type: 'array', + title: 'Assigned Users' }, - responded_user_name: { + responded_by_user: { anyOf: [ { - type: 'string' + '$ref': '#/components/schemas/HITLUser' }, { type: 'null' } - ], - title: 'Responded User Name' + ] }, response_at: { anyOf: [ @@ -3672,13 +3653,8 @@ export const $HITLDetailCollection = { export const $HITLDetailResponse = { properties: { - responded_user_id: { - type: 'string', - title: 'Responded User Id' - }, - responded_user_name: { - type: 'string', - title: 'Responded User Name' + responded_by: { + '$ref': '#/components/schemas/HITLUser' }, response_at: { type: 'string', @@ -3700,11 +3676,28 @@ export const $HITLDetailResponse = { } }, type: 'object', - required: ['responded_user_id', 'responded_user_name', 'response_at', 'chosen_options'], + required: ['responded_by', 'response_at', 'chosen_options'], title: 'HITLDetailResponse', description: 'Response of updating a Human-in-the-loop detail.' } as const; +export const $HITLUser = { + properties: { + id: { + type: 'string', + title: 'Id' + }, + name: { + type: 'string', + title: 'Name' + } + }, + type: 'object', + required: ['id', 'name'], + title: 'HITLUser', + description: 'Schema for a Human-in-the-loop users.' +} as const; + export const $HTTPExceptionResponse = { properties: { detail: { diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 5c944d4bc6a..ed570c2fc9d 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -3623,8 +3623,8 @@ export class HumanInTheLoopService { * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.state * @param data.responseReceived - * @param data.respondedUserId - * @param data.respondedUserName + * @param data.respondedByUserId + * @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns HITLDetailCollection Successful Response @@ -3645,8 +3645,8 @@ export class HumanInTheLoopService { task_id_pattern: data.taskIdPattern, state: data.state, response_received: data.responseReceived, - responded_user_id: data.respondedUserId, - responded_user_name: data.respondedUserName, + responded_by_user_id: data.respondedByUserId, + responded_by_user_name: data.respondedByUserName, subject_search: data.subjectSearch, body_search: data.bodySearch }, diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index c1320c1c635..e88e743d5b5 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -940,9 +940,8 @@ export type HITLDetail = { params?: { [key: string]: unknown; }; - respondents?: Array<(string)> | null; - responded_user_id?: string | null; - responded_user_name?: string | null; + assigned_users?: Array<HITLUser>; + responded_by_user?: HITLUser | null; response_at?: string | null; chosen_options?: Array<(string)> | null; params_input?: { @@ -963,8 +962,7 @@ export type HITLDetailCollection = { * Response of updating a Human-in-the-loop detail. */ export type HITLDetailResponse = { - responded_user_id: string; - responded_user_name: string; + responded_by: HITLUser; response_at: string; chosen_options: Array<(string)>; params_input?: { @@ -972,6 +970,14 @@ export type HITLDetailResponse = { }; }; +/** + * Schema for a Human-in-the-loop users. + */ +export type HITLUser = { + id: string; + name: string; +}; + /** * HTTPException Model used for error response. */ @@ -3100,8 +3106,8 @@ export type GetHitlDetailsData = { limit?: number; offset?: number; orderBy?: Array<(string)>; - respondedUserId?: Array<(string)>; - respondedUserName?: Array<(string)>; + respondedByUserId?: Array<(string)>; + respondedByUserName?: Array<(string)>; responseReceived?: boolean | null; state?: Array<(string)>; /** diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py index 9dfd3559920..e20f9fabba4 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py @@ -31,6 +31,7 @@ from sqlalchemy.orm import Session from airflow._shared.timezones.timezone import utcnow from airflow.models.hitl import HITLDetail from airflow.models.log import Log +from airflow.sdk.execution_time.hitl import HITLUser from airflow.utils.state import TaskInstanceState if TYPE_CHECKING: @@ -77,7 +78,7 @@ def sample_hitl_detail(sample_ti: TaskInstance, session: Session) -> HITLDetail: defaults=["Approve"], multiple=False, params={"input_1": 1}, - respondents=None, + assignees=None, ) session.add(hitl_detail_model) session.commit() @@ -95,7 +96,7 @@ def sample_hitl_detail_non_respondent(sample_ti: TaskInstance, session: Session) defaults=["Approve"], multiple=False, params={"input_1": 1}, - respondents=["non_test"], + assignees=[HITLUser(id="non_test", name="non_test")], ) session.add(hitl_detail_model) session.commit() @@ -113,7 +114,7 @@ def sample_hitl_detail_respondent(sample_ti: TaskInstance, session: Session) -> defaults=["Approve"], multiple=False, params={"input_1": 1}, - respondents=["test"], + assignees=[HITLUser(id="test", name="test")], ) session.add(hitl_detail_model) session.commit() @@ -173,8 +174,7 @@ def sample_hitl_details(sample_tis: list[TaskInstance], session: Session) -> lis response_at=utcnow(), chosen_options=[str(i)], params_input={"input": i}, - responded_user_id="test", - responded_user_name="test", + responded_by={"id": "test", "name": "test"}, ) for i, ti in enumerate(sample_tis[5:]) ] @@ -208,14 +208,13 @@ def expected_sample_hitl_detail_dict(sample_ti: TaskInstance) -> dict[str, Any]: "multiple": False, "options": ["Approve", "Reject"], "params": {"input_1": 1}, - "respondents": None, + "assigned_users": [], "params_input": {}, "response_at": None, "chosen_options": None, "response_received": False, "subject": "This is subject", - "responded_user_id": None, - "responded_user_name": None, + "responded_by_user": None, "task_instance": { "dag_display_name": DAG_ID, "dag_id": DAG_ID, @@ -321,8 +320,7 @@ class TestUpdateHITLDetailEndpoint: assert response.json() == { "params_input": {"input_1": 2}, "chosen_options": ["Approve"], - "responded_user_id": "test", - "responded_user_name": "test", + "responded_by": {"id": "test", "name": "test"}, "response_at": "2025-07-03T00:00:00Z", } @@ -332,7 +330,7 @@ class TestUpdateHITLDetailEndpoint: @time_machine.travel(datetime(2025, 7, 3, 0, 0, 0), tick=False) @pytest.mark.usefixtures("sample_hitl_detail_respondent") @pytest.mark.parametrize("map_index", [None, -1]) - def test_should_respond_200_to_respondent_user( + def test_should_respond_200_to_assigned_users( self, test_client: TestClient, sample_ti_url_identifier: str, @@ -351,8 +349,7 @@ class TestUpdateHITLDetailEndpoint: assert response.json() == { "params_input": {"input_1": 2}, "chosen_options": ["Approve"], - "responded_user_id": "test", - "responded_user_name": "test", + "responded_by": {"id": "test", "name": "test"}, "response_at": "2025-07-03T00:00:00Z", } @@ -453,8 +450,7 @@ class TestUpdateHITLDetailEndpoint: expected_response = { "params_input": {"input_1": 2}, "chosen_options": ["Approve"], - "responded_user_id": "test", - "responded_user_name": "test", + "responded_by": {"id": "test", "name": "test"}, "response_at": "2025-07-03T00:00:00Z", } assert response.status_code == 200 @@ -579,8 +575,8 @@ class TestGetHITLDetailsEndpoint: ({"body_search": "this is"}, 8), ({"response_received": False}, 5), ({"response_received": True}, 3), - ({"responded_user_id": ["test"]}, 3), - ({"responded_user_name": ["test"]}, 3), + ({"responded_by_user_id": ["test"]}, 3), + ({"responded_by_user_name": ["test"]}, 3), ], ids=[ "dag_id_pattern_hitl_dag", diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py index ea1f6293151..0272b584ab6 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py @@ -161,7 +161,7 @@ class TestGetDagRuns(TestPublicDagEndpoint): defaults=["Approve"], response_at=utcnow(), chosen_options=["Approve"], - responded_user_id="test", + responded_by={"id": "test", "name": "test"}, ) for i in range(3, 5) ] @@ -187,6 +187,7 @@ class TestGetDagRuns(TestPublicDagEndpoint): "params": {}, "params_input": {}, "response_received": False, + "assigned_users": [], } for i in range(3) ], diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py index 085fc7ae6a8..764d5a48ccd 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py @@ -45,13 +45,12 @@ default_hitl_detail_request_kwargs: dict[str, Any] = { "defaults": ["Approve"], "multiple": False, "params": {"input_1": 1}, - "respondents": None, + "assignees": None, } expected_empty_hitl_detail_response_part: dict[str, Any] = { "response_at": None, "chosen_options": None, - "responded_user_id": None, - "responded_user_name": None, + "responded_by_user": None, "params_input": {}, "response_received": False, } @@ -94,8 +93,7 @@ def expected_sample_hitl_detail_dict(sample_ti: TaskInstance) -> dict[str, Any]: "params_input": {"input_1": 2}, "response_at": convert_to_utc(datetime(2025, 7, 3, 0, 0, 0)), "chosen_options": ["Reject"], - "responded_user_id": "Fallback to defaults", - "responded_user_name": "Fallback to defaults", + "responded_by": {"id": "Fallback to defaults", "name": "Fallback to defaults"}, }, }, ], @@ -125,11 +123,15 @@ def test_upsert_hitl_detail( **default_hitl_detail_request_kwargs, }, ) - assert response.status_code == 201 - assert response.json() == { + + expected_json = { "ti_id": ti.id, **default_hitl_detail_request_kwargs, } + expected_json["assigned_users"] = expected_json.pop("assignees") or [] + + assert response.status_code == 201 + assert response.json() == expected_json def test_upsert_hitl_detail_with_empty_option( @@ -172,8 +174,10 @@ def test_update_hitl_detail(client: Client, sample_ti: TaskInstance) -> None: "response_at": "2025-07-03T00:00:00Z", "chosen_options": ["Reject"], "response_received": True, - "responded_user_id": "Fallback to defaults", - "responded_user_name": "Fallback to defaults", + "responded_by_user": { + "id": "Fallback to defaults", + "name": "Fallback to defaults", + }, } diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 37ecd597a5c..ccad0f23ed8 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -525,16 +525,13 @@ class FastAPIRootMiddlewareResponse(BaseModel): name: Annotated[str, Field(title="Name")] -class HITLDetailResponse(BaseModel): +class HITLUser(BaseModel): """ - Response of updating a Human-in-the-loop detail. + Schema for a Human-in-the-loop users. """ - responded_user_id: Annotated[str, Field(title="Responded User Id")] - responded_user_name: Annotated[str, Field(title="Responded User Name")] - response_at: Annotated[datetime, Field(title="Response At")] - chosen_options: Annotated[list[str], Field(min_length=1, title="Chosen Options")] - params_input: Annotated[dict[str, Any] | None, Field(title="Params Input")] = None + id: Annotated[str, Field(title="Id")] + name: Annotated[str, Field(title="Name")] class HTTPExceptionResponse(BaseModel): @@ -1434,6 +1431,17 @@ class EventLogCollectionResponse(BaseModel): total_entries: Annotated[int, Field(title="Total Entries")] +class HITLDetailResponse(BaseModel): + """ + Response of updating a Human-in-the-loop detail. + """ + + responded_by: HITLUser + response_at: Annotated[datetime, Field(title="Response At")] + chosen_options: Annotated[list[str], Field(min_length=1, title="Chosen Options")] + params_input: Annotated[dict[str, Any] | None, Field(title="Params Input")] = None + + class HTTPValidationError(BaseModel): detail: Annotated[list[ValidationError] | None, Field(title="Detail")] = None @@ -1828,9 +1836,8 @@ class HITLDetail(BaseModel): defaults: Annotated[list[str] | None, Field(title="Defaults")] = None multiple: Annotated[bool | None, Field(title="Multiple")] = False params: Annotated[dict[str, Any] | None, Field(title="Params")] = None - respondents: Annotated[list[str] | None, Field(title="Respondents")] = None - responded_user_id: Annotated[str | None, Field(title="Responded User Id")] = None - responded_user_name: Annotated[str | None, Field(title="Responded User Name")] = None + assigned_users: Annotated[list[HITLUser] | None, Field(title="Assigned Users")] = None + responded_by_user: HITLUser | None = None response_at: Annotated[datetime | None, Field(title="Response At")] = None chosen_options: Annotated[list[str] | None, Field(title="Chosen Options")] = None params_input: Annotated[dict[str, Any] | None, Field(title="Params Input")] = None diff --git a/providers/standard/src/airflow/providers/standard/example_dags/example_hitl_operator.py b/providers/standard/src/airflow/providers/standard/example_dags/example_hitl_operator.py index dd7b4dc277f..44d77ba4959 100644 --- a/providers/standard/src/airflow/providers/standard/example_dags/example_hitl_operator.py +++ b/providers/standard/src/airflow/providers/standard/example_dags/example_hitl_operator.py @@ -140,6 +140,7 @@ with DAG( notifiers=[hitl_request_callback], on_success_callback=hitl_success_callback, on_failure_callback=hitl_failure_callback, + assigned_users=[{"id": "admin", "name": "admin"}], ) # [END howto_hitl_approval_operator] diff --git a/providers/standard/src/airflow/providers/standard/operators/hitl.py b/providers/standard/src/airflow/providers/standard/operators/hitl.py index 440abf9a516..f8e1c631ea6 100644 --- a/providers/standard/src/airflow/providers/standard/operators/hitl.py +++ b/providers/standard/src/airflow/providers/standard/operators/hitl.py @@ -41,6 +41,7 @@ from airflow.sdk.timezone import utcnow if TYPE_CHECKING: from airflow.sdk.definitions.context import Context + from airflow.sdk.execution_time.hitl import HITLUser from airflow.sdk.types import RuntimeTaskInstanceProtocol @@ -70,7 +71,7 @@ class HITLOperator(BaseOperator): multiple: bool = False, params: ParamsDict | dict[str, Any] | None = None, notifiers: Sequence[BaseNotifier] | BaseNotifier | None = None, - respondents: str | list[str] | None = None, + assigned_users: HITLUser | list[HITLUser] | None = None, **kwargs, ) -> None: super().__init__(**kwargs) @@ -86,7 +87,7 @@ class HITLOperator(BaseOperator): self.notifiers: Sequence[BaseNotifier] = ( [notifiers] if isinstance(notifiers, BaseNotifier) else notifiers or [] ) - self.respondents = [respondents] if isinstance(respondents, str) else respondents + self.assigned_users = [assigned_users] if isinstance(assigned_users, dict) else assigned_users self.validate_options() self.validate_params() @@ -138,7 +139,7 @@ class HITLOperator(BaseOperator): defaults=self.defaults, multiple=self.multiple, params=self.serialized_params, - respondents=self.respondents, + assigned_users=self.assigned_users, ) if self.execution_timeout: @@ -178,6 +179,7 @@ class HITLOperator(BaseOperator): return HITLTriggerEventSuccessPayload( chosen_options=chosen_options, params_input=params_input, + responded_by_user=event["responded_by_user"], ) def process_trigger_event_error(self, event: dict[str, Any]) -> None: diff --git a/providers/standard/src/airflow/providers/standard/triggers/hitl.py b/providers/standard/src/airflow/providers/standard/triggers/hitl.py index c5f32800058..a9299ae2407 100644 --- a/providers/standard/src/airflow/providers/standard/triggers/hitl.py +++ b/providers/standard/src/airflow/providers/standard/triggers/hitl.py @@ -25,12 +25,13 @@ if not AIRFLOW_V_3_1_PLUS: import asyncio from collections.abc import AsyncIterator from datetime import datetime -from typing import Any, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, TypedDict from uuid import UUID from asgiref.sync import sync_to_async from airflow.sdk.execution_time.hitl import ( + HITLUser, get_hitl_detail_content_detail, update_hitl_detail_response, ) @@ -43,6 +44,7 @@ class HITLTriggerEventSuccessPayload(TypedDict, total=False): chosen_options: list[str] params_input: dict[str, Any] + responded_by_user: HITLUser timedout: bool @@ -100,12 +102,15 @@ class HITLTrigger(BaseTrigger): if self.timeout_datetime and self.timeout_datetime < utcnow(): # Fetch latest HITL detail before fallback resp = await sync_to_async(get_hitl_detail_content_detail)(ti_id=self.ti_id) + # Response already received, yield success and exit if resp.response_received and resp.chosen_options: - # Response already received, yield success and exit + if TYPE_CHECKING: + assert resp.responded_by_user is not None + self.log.info( "[HITL] responded_by=%s (id=%s) options=%s at %s (timeout fallback skipped)", - resp.responded_user_name, - resp.responded_user_id, + resp.responded_by_user.name, + resp.responded_by_user.id, resp.chosen_options, resp.response_at, ) @@ -113,6 +118,10 @@ class HITLTrigger(BaseTrigger): HITLTriggerEventSuccessPayload( chosen_options=resp.chosen_options, params_input=resp.params_input or {}, + responded_by_user=HITLUser( + id=resp.responded_by_user.id, + name=resp.responded_by_user.name, + ), timedout=False, ) ) @@ -139,6 +148,10 @@ class HITLTrigger(BaseTrigger): HITLTriggerEventSuccessPayload( chosen_options=self.defaults, params_input=self.params, + responded_by_user=HITLUser( + id="Fallback to defaults", + name="Fallback to defaults", + ), timedout=True, ) ) @@ -146,10 +159,12 @@ class HITLTrigger(BaseTrigger): resp = await sync_to_async(get_hitl_detail_content_detail)(ti_id=self.ti_id) if resp.response_received and resp.chosen_options: + if TYPE_CHECKING: + assert resp.responded_by_user is not None self.log.info( "[HITL] responded_by=%s (id=%s) options=%s at %s", - resp.responded_user_name, - resp.responded_user_id, + resp.responded_by_user.name, + resp.responded_by_user.id, resp.chosen_options, resp.response_at, ) @@ -157,6 +172,10 @@ class HITLTrigger(BaseTrigger): HITLTriggerEventSuccessPayload( chosen_options=resp.chosen_options, params_input=resp.params_input or {}, + responded_by_user=HITLUser( + id=resp.responded_by_user.id, + name=resp.responded_by_user.name, + ), timedout=False, ) ) diff --git a/providers/standard/tests/unit/standard/operators/test_hitl.py b/providers/standard/tests/unit/standard/operators/test_hitl.py index 18646ab118d..98d0bf66864 100644 --- a/providers/standard/tests/unit/standard/operators/test_hitl.py +++ b/providers/standard/tests/unit/standard/operators/test_hitl.py @@ -43,6 +43,7 @@ from airflow.providers.standard.operators.hitl import ( ) from airflow.sdk import Param, timezone from airflow.sdk.definitions.param import ParamsDict +from airflow.sdk.execution_time.hitl import HITLUser from tests_common.test_utils.config import conf_vars @@ -66,7 +67,7 @@ def hitl_task_and_ti_for_generating_link(dag_maker: DagMaker) -> tuple[HITLOpera options=["1", "2", "3", "4", "5"], body="This is body", defaults=["1"], - respondents="test", + assigned_users=HITLUser(id="test", name="test"), multiple=True, params=ParamsDict({"input_1": 1, "input_2": 2, "input_3": 3}), ) @@ -165,7 +166,7 @@ class TestHITLOperator: options=["1", "2", "3", "4", "5"], body="This is body", defaults=["1"], - respondents="test", + assigned_users=HITLUser(id="test", name="test"), multiple=False, params=ParamsDict({"input_1": 1}), notifiers=[notifier], @@ -181,10 +182,9 @@ class TestHITLOperator: assert hitl_detail_model.defaults == ["1"] assert hitl_detail_model.multiple is False assert hitl_detail_model.params == {"input_1": 1} - assert hitl_detail_model.respondents == ["test"] + assert hitl_detail_model.assignees == [{"id": "test", "name": "test"}] assert hitl_detail_model.response_at is None - assert hitl_detail_model.responded_user_id is None - assert hitl_detail_model.responded_user_name is None + assert hitl_detail_model.responded_by is None assert hitl_detail_model.chosen_options is None assert hitl_detail_model.params_input == {} @@ -232,7 +232,11 @@ class TestHITLOperator: ret = hitl_op.execute_complete( context={}, - event={"chosen_options": ["1"], "params_input": {"input": 2}}, + event={ + "chosen_options": ["1"], + "params_input": {"input": 2}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) assert ret["chosen_options"] == ["1"] @@ -253,6 +257,7 @@ class TestHITLOperator: event={ "chosen_options": ["not exists"], "params_input": {"input": 2}, + "responded_by_user": {"id": "test", "name": "test"}, }, ) @@ -271,6 +276,7 @@ class TestHITLOperator: event={ "chosen_options": ["1"], "params_input": {"no such key": 2, "input": 333}, + "responded_by_user": {"id": "test", "name": "test"}, }, ) @@ -397,12 +403,17 @@ class TestApprovalOperator: ret = hitl_op.execute_complete( context={}, - event={"chosen_options": ["Approve"], "params_input": {}}, + event={ + "chosen_options": ["Approve"], + "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) assert ret == { "chosen_options": ["Approve"], "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, } def test_execute_complete_with_downstream_tasks(self, dag_maker) -> None: @@ -419,7 +430,11 @@ class TestApprovalOperator: with pytest.raises(DownstreamTasksSkipped) as exc_info: hitl_op.execute_complete( context={"ti": ti, "task": ti.task}, - event={"chosen_options": ["Reject"], "params_input": {}}, + event={ + "chosen_options": ["Reject"], + "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) assert set(exc_info.value.tasks) == {"op1"} @@ -480,6 +495,7 @@ class TestHITLBranchOperator: event={ "chosen_options": ["branch_1"], "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, }, ) assert set(exc_info.value.tasks) == set((f"branch_{i}", -1) for i in range(2, 6)) @@ -503,6 +519,7 @@ class TestHITLBranchOperator: event={ "chosen_options": [f"branch_{i}" for i in range(1, 4)], "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, }, ) assert set(exc_info.value.tasks) == set((f"branch_{i}", -1) for i in range(4, 6)) @@ -524,7 +541,11 @@ class TestHITLBranchOperator: with pytest.raises(DownstreamTasksSkipped) as exc: op.execute_complete( context={"ti": ti, "task": ti.task}, - event={"chosen_options": ["Approve"], "params_input": {}}, + event={ + "chosen_options": ["Approve"], + "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) # checks to see that the "archive" task was skipped assert set(exc.value.tasks) == {("archive", -1)} @@ -551,7 +572,11 @@ class TestHITLBranchOperator: with pytest.raises(DownstreamTasksSkipped) as exc: op.execute_complete( context={"ti": ti, "task": ti.task}, - event={"chosen_options": ["Approve", "KeepAsIs"], "params_input": {}}, + event={ + "chosen_options": ["Approve", "KeepAsIs"], + "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) # publish + keep chosen → only "other" skipped assert set(exc.value.tasks) == {("other", -1)} @@ -572,7 +597,11 @@ class TestHITLBranchOperator: with pytest.raises(DownstreamTasksSkipped) as exc: op.execute_complete( context={"ti": ti, "task": ti.task}, - event={"chosen_options": ["branch_2"], "params_input": {}}, + event={ + "chosen_options": ["branch_2"], + "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) assert set(exc.value.tasks) == {("branch_1", -1)} @@ -593,7 +622,11 @@ class TestHITLBranchOperator: with pytest.raises(AirflowException, match="downstream|not found"): op.execute_complete( context={"ti": ti, "task": ti.task}, - event={"chosen_options": ["Approve"], "params_input": {}}, + event={ + "chosen_options": ["Approve"], + "params_input": {}, + "responded_by_user": {"id": "test", "name": "test"}, + }, ) @pytest.mark.parametrize("bad", [123, ["publish"], {"x": "y"}, b"publish"]) diff --git a/providers/standard/tests/unit/standard/triggers/test_hitl.py b/providers/standard/tests/unit/standard/triggers/test_hitl.py index 7b33715e68d..ef82f9e09d9 100644 --- a/providers/standard/tests/unit/standard/triggers/test_hitl.py +++ b/providers/standard/tests/unit/standard/triggers/test_hitl.py @@ -31,7 +31,7 @@ from unittest import mock from uuid6 import uuid7 from airflow._shared.timezones.timezone import utc, utcnow -from airflow.api_fastapi.execution_api.datamodels.hitl import HITLDetailResponse +from airflow.api_fastapi.execution_api.datamodels.hitl import HITLDetailResponse, HITLUser from airflow.providers.standard.triggers.hitl import ( HITLTrigger, HITLTriggerEventFailurePayload, @@ -110,8 +110,7 @@ class TestHITLTrigger: ) mock_supervisor_comms.send.return_value = HITLDetailResponse( response_received=False, - responded_user_id=None, - responded_user_name=None, + responded_by_user=None, response_at=None, chosen_options=None, params_input={}, @@ -123,7 +122,12 @@ class TestHITLTrigger: event = await trigger_task assert event == TriggerEvent( - HITLTriggerEventSuccessPayload(chosen_options=["1"], params_input={"input": 1}, timedout=True) + HITLTriggerEventSuccessPayload( + chosen_options=["1"], + params_input={"input": 1}, + responded_by_user={"id": "Fallback to defaults", "name": "Fallback to defaults"}, + timedout=True, + ) ) assert mock_log.info.call_args == mock.call( @@ -149,8 +153,7 @@ class TestHITLTrigger: ) mock_supervisor_comms.send.return_value = HITLDetailResponse( response_received=True, - responded_user_id="1", - responded_user_name="test", + responded_by_user=HITLUser(id="1", name="test"), response_at=action_datetime, chosen_options=["2"], params_input={}, @@ -162,7 +165,12 @@ class TestHITLTrigger: event = await trigger_task assert event == TriggerEvent( - HITLTriggerEventSuccessPayload(chosen_options=["2"], params_input={}, timedout=False) + HITLTriggerEventSuccessPayload( + chosen_options=["2"], + params_input={}, + responded_by_user={"id": "1", "name": "test"}, + timedout=False, + ) ) assert mock_log.info.call_args == mock.call( @@ -188,8 +196,7 @@ class TestHITLTrigger: ) mock_supervisor_comms.send.return_value = HITLDetailResponse( response_received=True, - responded_user_id="test", - responded_user_name="test", + responded_by_user=HITLUser(id="test", name="test"), response_at=utcnow(), chosen_options=["3"], params_input={"input": 50}, @@ -203,6 +210,7 @@ class TestHITLTrigger: HITLTriggerEventSuccessPayload( chosen_options=["3"], params_input={"input": 50}, + responded_by_user={"id": "test", "name": "test"}, timedout=False, ) ) diff --git a/task-sdk/src/airflow/sdk/api/client.py b/task-sdk/src/airflow/sdk/api/client.py index 04575fa770d..0a353271c10 100644 --- a/task-sdk/src/airflow/sdk/api/client.py +++ b/task-sdk/src/airflow/sdk/api/client.py @@ -44,6 +44,7 @@ from airflow.sdk.api.datamodels._generated import ( DagRunStateResponse, DagRunType, HITLDetailResponse, + HITLUser, InactiveAssetsResponse, PrevSuccessfulDagRunResponse, TaskInstanceState, @@ -723,7 +724,7 @@ class HITLOperations: defaults: list[str] | None = None, multiple: bool = False, params: dict[str, Any] | None = None, - respondents: list[str] | None = None, + assigned_users: list[HITLUser] | None = None, ) -> HITLDetailRequestResult: """Add a Human-in-the-loop response that waits for human response for a specific Task Instance.""" payload = CreateHITLDetailPayload( @@ -734,7 +735,7 @@ class HITLOperations: defaults=defaults, multiple=multiple, params=params, - respondents=respondents, + assigned_users=assigned_users, ) resp = self.client.post( f"/hitlDetails/{ti_id}", diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py index cd861bf07d1..d2151d59d02 100644 --- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py +++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py @@ -154,32 +154,13 @@ class DagRunType(str, Enum): ASSET_TRIGGERED = "asset_triggered" -class HITLDetailRequest(BaseModel): +class HITLUser(BaseModel): """ - Schema for the request part of a Human-in-the-loop detail for a specific task instance. + Schema for a Human-in-the-loop users. """ - ti_id: Annotated[UUID, Field(title="Ti Id")] - options: Annotated[list[str], Field(min_length=1, title="Options")] - subject: Annotated[str, Field(title="Subject")] - body: Annotated[str | None, Field(title="Body")] = None - defaults: Annotated[list[str] | None, Field(title="Defaults")] = None - multiple: Annotated[bool | None, Field(title="Multiple")] = False - params: Annotated[dict[str, Any] | None, Field(title="Params")] = None - respondents: Annotated[list[str] | None, Field(title="Respondents")] = None - - -class HITLDetailResponse(BaseModel): - """ - Schema for the response part of a Human-in-the-loop detail for a specific task instance. - """ - - response_received: Annotated[bool, Field(title="Response Received")] - responded_user_name: Annotated[str | None, Field(title="Responded User Name")] = None - responded_user_id: Annotated[str | None, Field(title="Responded User Id")] = None - response_at: Annotated[AwareDatetime | None, Field(title="Response At")] = None - chosen_options: Annotated[list[str] | None, Field(title="Chosen Options")] = None - params_input: Annotated[dict[str, Any] | None, Field(title="Params Input")] = None + id: Annotated[str, Field(title="Id")] + name: Annotated[str, Field(title="Name")] class InactiveAssetsResponse(BaseModel): @@ -561,6 +542,33 @@ class DagRun(BaseModel): consumed_asset_events: Annotated[list[AssetEventDagRunReference], Field(title="Consumed Asset Events")] +class HITLDetailRequest(BaseModel): + """ + Schema for the request part of a Human-in-the-loop detail for a specific task instance. + """ + + ti_id: Annotated[UUID, Field(title="Ti Id")] + options: Annotated[list[str], Field(min_length=1, title="Options")] + subject: Annotated[str, Field(title="Subject")] + body: Annotated[str | None, Field(title="Body")] = None + defaults: Annotated[list[str] | None, Field(title="Defaults")] = None + multiple: Annotated[bool | None, Field(title="Multiple")] = False + params: Annotated[dict[str, Any] | None, Field(title="Params")] = None + assigned_users: Annotated[list[HITLUser] | None, Field(title="Assigned Users")] = None + + +class HITLDetailResponse(BaseModel): + """ + Schema for the response part of a Human-in-the-loop detail for a specific task instance. + """ + + response_received: Annotated[bool, Field(title="Response Received")] + responded_by_user: HITLUser | None = None + response_at: Annotated[AwareDatetime | None, Field(title="Response At")] = None + chosen_options: Annotated[list[str] | None, Field(title="Chosen Options")] = None + params_input: Annotated[dict[str, Any] | None, Field(title="Params Input")] = None + + class HTTPValidationError(BaseModel): detail: Annotated[list[ValidationError] | None, Field(title="Detail")] = None diff --git a/task-sdk/src/airflow/sdk/execution_time/hitl.py b/task-sdk/src/airflow/sdk/execution_time/hitl.py index 500be78ab4e..07f94e63c05 100644 --- a/task-sdk/src/airflow/sdk/execution_time/hitl.py +++ b/task-sdk/src/airflow/sdk/execution_time/hitl.py @@ -17,9 +17,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict from uuid import UUID +from airflow.sdk.api.datamodels._generated import HITLUser as APIHITLUser from airflow.sdk.execution_time.comms import ( CreateHITLDetailPayload, GetHITLDetailResponse, @@ -30,6 +31,11 @@ if TYPE_CHECKING: from airflow.sdk.api.datamodels._generated import HITLDetailResponse +class HITLUser(TypedDict): + id: str + name: str + + def upsert_hitl_detail( ti_id: UUID, options: list[str], @@ -38,7 +44,7 @@ def upsert_hitl_detail( defaults: list[str] | None = None, multiple: bool = False, params: dict[str, Any] | None = None, - respondents: list[str] | None = None, + assigned_users: list[HITLUser] | None = None, ) -> None: from airflow.sdk.execution_time.task_runner import SUPERVISOR_COMMS @@ -51,7 +57,11 @@ def upsert_hitl_detail( defaults=defaults, params=params, multiple=multiple, - respondents=respondents, + assigned_users=( + [APIHITLUser(id=user["id"], name=user["name"]) for user in assigned_users] + if assigned_users + else [] + ), ) ) diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py b/task-sdk/src/airflow/sdk/execution_time/supervisor.py index 76a52acd98f..4969ed8a1d4 100644 --- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py +++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py @@ -1344,7 +1344,7 @@ class ActivitySubprocess(WatchedSubprocess): defaults=msg.defaults, params=msg.params, multiple=msg.multiple, - respondents=msg.respondents, + assigned_users=msg.assigned_users, ) self.send_msg(resp, request_id=req_id, error=None, **dump_opts) elif isinstance(msg, MaskSecret): diff --git a/task-sdk/tests/task_sdk/api/test_client.py b/task-sdk/tests/task_sdk/api/test_client.py index 986ab5fe5e8..9464296934d 100644 --- a/task-sdk/tests/task_sdk/api/test_client.py +++ b/task-sdk/tests/task_sdk/api/test_client.py @@ -38,6 +38,7 @@ from airflow.sdk.api.datamodels._generated import ( DagRunState, DagRunStateResponse, HITLDetailResponse, + HITLUser, VariableResponse, XComResponse, ) @@ -1289,7 +1290,7 @@ class TestHITLOperations: assert result.defaults == ["Approval"] assert result.params is None assert result.multiple is False - assert result.respondents is None + assert result.assigned_users is None def test_update_response(self, time_machine: TimeMachineFixture) -> None: time_machine.move_to(datetime(2025, 7, 3, 0, 0, 0)) @@ -1302,8 +1303,7 @@ class TestHITLOperations: json={ "chosen_options": ["Approval"], "params_input": {}, - "responded_user_id": "admin", - "responded_user_name": "admin", + "responded_by_user": {"id": "admin", "name": "admin"}, "response_received": True, "response_at": "2025-07-03T00:00:00Z", }, @@ -1320,8 +1320,7 @@ class TestHITLOperations: assert result.response_received is True assert result.chosen_options == ["Approval"] assert result.params_input == {} - assert result.responded_user_id == "admin" - assert result.responded_user_name == "admin" + assert result.responded_by_user == HITLUser(id="admin", name="admin") assert result.response_at == timezone.datetime(2025, 7, 3, 0, 0, 0) def test_get_detail_response(self, time_machine: TimeMachineFixture) -> None: @@ -1335,8 +1334,7 @@ class TestHITLOperations: json={ "chosen_options": ["Approval"], "params_input": {}, - "responded_user_id": "admin", - "responded_user_name": "admin", + "responded_by_user": {"id": "admin", "name": "admin"}, "response_received": True, "response_at": "2025-07-03T00:00:00Z", }, @@ -1349,6 +1347,5 @@ class TestHITLOperations: assert result.response_received is True assert result.chosen_options == ["Approval"] assert result.params_input == {} - assert result.responded_user_id == "admin" - assert result.responded_user_name == "admin" + assert result.responded_by_user == HITLUser(id="admin", name="admin") assert result.response_at == timezone.datetime(2025, 7, 3, 0, 0, 0) diff --git a/task-sdk/tests/task_sdk/execution_time/test_hitl.py b/task-sdk/tests/task_sdk/execution_time/test_hitl.py index a5ed016f44d..ad74ade5cd2 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_hitl.py +++ b/task-sdk/tests/task_sdk/execution_time/test_hitl.py @@ -20,9 +20,10 @@ from __future__ import annotations from uuid6 import uuid7 from airflow.sdk import timezone -from airflow.sdk.api.datamodels._generated import HITLDetailResponse +from airflow.sdk.api.datamodels._generated import HITLDetailResponse, HITLUser as APIHITLUser from airflow.sdk.execution_time.comms import CreateHITLDetailPayload from airflow.sdk.execution_time.hitl import ( + HITLUser, get_hitl_detail_content_detail, update_hitl_detail_response, upsert_hitl_detail, @@ -39,7 +40,7 @@ def test_upsert_hitl_detail(mock_supervisor_comms) -> None: body="Optional body", defaults=["Approve", "Reject"], params={"input_1": 1}, - respondents=["test"], + assigned_users=[HITLUser(id="test", name="test")], multiple=False, ) mock_supervisor_comms.send.assert_called_with( @@ -50,7 +51,7 @@ def test_upsert_hitl_detail(mock_supervisor_comms) -> None: body="Optional body", defaults=["Approve", "Reject"], params={"input_1": 1}, - respondents=["test"], + assigned_users=[APIHITLUser(id="test", name="test")], multiple=False, ) ) @@ -62,8 +63,7 @@ def test_update_hitl_detail_response(mock_supervisor_comms) -> None: response_received=True, chosen_options=["Approve"], response_at=timestamp, - responded_user_id="admin", - responded_user_name="admin", + responded_by_user=APIHITLUser(id="admin", name="admin"), params_input={"input_1": 1}, ) resp = update_hitl_detail_response( @@ -75,8 +75,7 @@ def test_update_hitl_detail_response(mock_supervisor_comms) -> None: response_received=True, chosen_options=["Approve"], response_at=timestamp, - responded_user_id="admin", - responded_user_name="admin", + responded_by_user=APIHITLUser(id="admin", name="admin"), params_input={"input_1": 1}, ) @@ -86,8 +85,7 @@ def test_get_hitl_detail_content_detail(mock_supervisor_comms) -> None: response_received=False, chosen_options=None, response_at=None, - responded_user_id=None, - responded_user_name=None, + responded_by_user=None, params_input={}, ) resp = get_hitl_detail_content_detail(TI_ID) @@ -95,7 +93,6 @@ def test_get_hitl_detail_content_detail(mock_supervisor_comms) -> None: response_received=False, chosen_options=None, response_at=None, - responded_user_id=None, - responded_user_name=None, + responded_by_user=None, params_input={}, ) diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py index ce60753bf6a..ae39595197a 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py +++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py @@ -2008,7 +2008,7 @@ REQUEST_TEST_CASES = [ "defaults": ["Approve"], "multiple": False, "params": {}, - "respondents": None, + "assigned_users": None, "type": "HITLDetailRequestResult", }, client_mock=ClientMock( @@ -2019,7 +2019,7 @@ REQUEST_TEST_CASES = [ "multiple": False, "options": ["Approve", "Reject"], "params": {}, - "respondents": None, + "assigned_users": None, "subject": "This is subject", "ti_id": TI_ID, },
