jscheffl commented on code in PR #52868: URL: https://github.com/apache/airflow/pull/52868#discussion_r2193399844
########## airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py: ########## @@ -0,0 +1,77 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from collections.abc import Mapping +from datetime import datetime +from typing import Any + +from pydantic import Field, field_validator + +from airflow.api_fastapi.core_api.base import BaseModel +from airflow.sdk import Param + + +class UpdateHITLResponsePayload(BaseModel): + """Schema for updating the content of a Human-in-the-loop response.""" + + response_content: list[str] + params_input: Mapping = Field(default_factory=dict) + + +class HITLResponseContentDetail(BaseModel): + """Response of updating a Human-in-the-loop response.""" + + user_id: str + response_at: datetime + response_content: list[str] + params_input: Mapping = Field(default_factory=dict) + + +class HITLResponseDetail(BaseModel): + """Schema for Human-in-the-loop response.""" + + ti_id: str + + # Input Request + options: list[str] + subject: str + body: str | None = None + default: list[str] | None = None + multiple: bool = False + params: dict[str, Any] = Field(default_factory=dict) + + # Response Content Detail + user_id: str | None = None + response_at: datetime | None = None + response_content: list[str] | None = None + params_input: dict[str, Any] = Field(default_factory=dict) Review Comment: That field should also be null-able as long as no user input made (like the fields above)? ########## airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py: ########## @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from uuid import UUID + +import structlog +from fastapi import Depends, HTTPException, status +from sqlalchemy import select + +from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity +from airflow.api_fastapi.common.db.common import SessionDep, paginated_select +from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.core_api.datamodels.hitl import ( + HITLResponseContentDetail, + HITLResponseDetail, + HITLResponseDetailCollection, + UpdateHITLResponsePayload, +) +from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc +from airflow.api_fastapi.core_api.security import GetUserDep, ReadableTIFilterDep, requires_access_dag +from airflow.models.hitl import HITLResponseModel +from airflow.models.taskinstance import TaskInstance as TI +from airflow.utils import timezone + +hitl_router = AirflowRouter(tags=["HumanInTheLoop"], prefix="/hitl-responses") + +log = structlog.get_logger(__name__) + + +@hitl_router.patch( + "/{task_instance_id}", + responses=create_openapi_http_exception_doc( + [ + status.HTTP_404_NOT_FOUND, + status.HTTP_409_CONFLICT, + ] + ), + dependencies=[ + Depends(requires_access_dag(method="GET", access_entity=DagAccessEntity.TASK_INSTANCE)), + ], +) +def update_hitl_response( + task_instance_id: UUID, + update_hitl_response_payload: UpdateHITLResponsePayload, + user: GetUserDep, + session: SessionDep, +) -> HITLResponseContentDetail: + """Update a Human-in-the-loop response.""" + ti_id_str = str(task_instance_id) + hitl_response_model = session.scalar( + select(HITLResponseModel).where(HITLResponseModel.ti_id == ti_id_str) + ) + if not hitl_response_model: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + f"Human-in-the-loop Response does not exist for Task Instance with id {ti_id_str}", + ) + + if hitl_response_model.response_received: Review Comment: Great! ...but... mhm... thinking a bit more... How do we treat the case when somebody clears the task state? Can the response be submitted another time? (I do not have an answer, I think we did not discuss this in detail....?) ########## airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py: ########## @@ -0,0 +1,71 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Add Human In the Loop Response table. + +Revision ID: 40f7c30a228b +Revises: ffdb0566c7c0 +Create Date: 2025-07-04 15:05:19.459197 + +""" + +from __future__ import annotations + +import sqlalchemy_jsonfield +from alembic import op +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql + +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + +# revision identifiers, used by Alembic. +revision = "40f7c30a228b" +down_revision = "ffdb0566c7c0" +branch_labels = None +depends_on = None +airflow_version = "3.1.0" + + +def upgrade(): + """Add Human In the Loop Response table.""" + op.create_table( + "hitl_response", Review Comment: I am asking myself if the name of the table is "good". Because 50% of columns also contain details of the request/task. Response is only the other half. Would "hitl_task" be more reasonable? Other ideas welcome. ########## providers/standard/src/airflow/providers/standard/operators/hitl.py: ########## @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from airflow.exceptions import AirflowOptionalProviderFeatureException +from airflow.providers.standard.version_compat import AIRFLOW_V_3_1_PLUS + +if not AIRFLOW_V_3_1_PLUS: + raise AirflowOptionalProviderFeatureException("Human in the loop functionality needs Airflow 3.1+.") + + +from collections.abc import Collection, Mapping +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +from airflow.models import SkipMixin +from airflow.models.baseoperator import BaseOperator +from airflow.providers.standard.exceptions import HITLTriggerEventError +from airflow.providers.standard.triggers.hitl import HITLTrigger, HITLTriggerEventSuccessPayload +from airflow.sdk.definitions.param import ParamsDict +from airflow.sdk.execution_time.hitl import add_hitl_response + +if TYPE_CHECKING: + from airflow.sdk.definitions.context import Context + + +class HITLOperator(BaseOperator): + """ + Base class for all Human-in-the-loop Operators to inherit from. + + :param subject: Headline/subject presented to the user for the interaction task. + :param options: List of options that the an user can select from to complete the task. + :param body: descriptive text that might give background, hints or can provide background or summary of + details that are needed to decide. Review Comment: I'd propose to note here as well that this will be rendered as Markdown on UI? (I think we should to have the options of basic text formatting and UI has a module for this already... ########## providers/standard/src/airflow/providers/standard/operators/hitl.py: ########## @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from airflow.exceptions import AirflowOptionalProviderFeatureException +from airflow.providers.standard.version_compat import AIRFLOW_V_3_1_PLUS + +if not AIRFLOW_V_3_1_PLUS: + raise AirflowOptionalProviderFeatureException("Human in the loop functionality needs Airflow 3.1+.") + + +from collections.abc import Collection, Mapping +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +from airflow.models import SkipMixin +from airflow.models.baseoperator import BaseOperator +from airflow.providers.standard.exceptions import HITLTriggerEventError +from airflow.providers.standard.triggers.hitl import HITLTrigger, HITLTriggerEventSuccessPayload +from airflow.sdk.definitions.param import ParamsDict +from airflow.sdk.execution_time.hitl import add_hitl_response + +if TYPE_CHECKING: + from airflow.sdk.definitions.context import Context + + +class HITLOperator(BaseOperator): + """ + Base class for all Human-in-the-loop Operators to inherit from. + + :param subject: Headline/subject presented to the user for the interaction task. + :param options: List of options that the an user can select from to complete the task. + :param body: descriptive text that might give background, hints or can provide background or summary of + details that are needed to decide. + :param default: The default option and the option that is taken if timeout is passed. + :param multiple: Whether the user can select one or multiple options. + :param params: dictionary of parameter definitions that are in the format of Dag params such that + a Form Field can be rendered. Entered data is validated (schema, required fields) like for a Dag run + and added to XCom of the task result. + """ + + template_fields: Collection[str] = ("subject", "body") + + def __init__( + self, + *, + subject: str, + options: list[str], + body: str | None = None, + default: str | list[str] | None = None, + multiple: bool = False, + params: ParamsDict | dict[str, Any] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.subject = subject + self.body = body + + self.options = options + # allow defaults to store more than one options when multiple=True + self.default = [default] if isinstance(default, str) else default + self.multiple = multiple + + self.params: ParamsDict | dict = params or {} Review Comment: Would it not be better to initialize a new ParamsDict() instead of noting two types? ```suggestion self.params: ParamsDict = params or ParamsDict() ``` ########## providers/standard/src/airflow/providers/standard/operators/hitl.py: ########## @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from airflow.exceptions import AirflowOptionalProviderFeatureException +from airflow.providers.standard.version_compat import AIRFLOW_V_3_1_PLUS + +if not AIRFLOW_V_3_1_PLUS: + raise AirflowOptionalProviderFeatureException("Human in the loop functionality needs Airflow 3.1+.") + + +from collections.abc import Collection, Mapping +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +from airflow.models import SkipMixin +from airflow.models.baseoperator import BaseOperator +from airflow.providers.standard.exceptions import HITLTriggerEventError +from airflow.providers.standard.triggers.hitl import HITLTrigger, HITLTriggerEventSuccessPayload +from airflow.sdk.definitions.param import ParamsDict +from airflow.sdk.execution_time.hitl import add_hitl_response + +if TYPE_CHECKING: + from airflow.sdk.definitions.context import Context + + +class HITLOperator(BaseOperator): + """ + Base class for all Human-in-the-loop Operators to inherit from. + + :param subject: Headline/subject presented to the user for the interaction task. + :param options: List of options that the an user can select from to complete the task. + :param body: descriptive text that might give background, hints or can provide background or summary of + details that are needed to decide. + :param default: The default option and the option that is taken if timeout is passed. + :param multiple: Whether the user can select one or multiple options. + :param params: dictionary of parameter definitions that are in the format of Dag params such that + a Form Field can be rendered. Entered data is validated (schema, required fields) like for a Dag run + and added to XCom of the task result. + """ + + template_fields: Collection[str] = ("subject", "body") + + def __init__( + self, + *, + subject: str, + options: list[str], + body: str | None = None, + default: str | list[str] | None = None, + multiple: bool = False, + params: ParamsDict | dict[str, Any] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.subject = subject + self.body = body + + self.options = options + # allow defaults to store more than one options when multiple=True + self.default = [default] if isinstance(default, str) else default + self.multiple = multiple + + self.params: ParamsDict | dict = params or {} Review Comment: Yeah... of course if the user provides a dict, needs to be converted to ParamsDict though... ########## airflow-core/src/airflow/models/hitl.py: ########## @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import sqlalchemy_jsonfield +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.hybrid import hybrid_property + +from airflow.models.base import Base +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + + +class HITLResponseModel(Base): Review Comment: With the comment above... also would prefer w/o "model" in the name. And alongside... the model does not contain "Reponse" but also "Request" as 50% is the user presented form and subject data. So would we maybe need to name it "HITL" only or "HITLTask"? ########## airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py: ########## @@ -48,5 +49,6 @@ ) authenticated_router.include_router(variables.router, prefix="/variables", tags=["Variables"]) authenticated_router.include_router(xcoms.router, prefix="/xcoms", tags=["XComs"]) +authenticated_router.include_router(hitl.router, prefix="/hitl-responses", tags=["Human in the Loop"]) Review Comment: I'd propose to leave the "responses" out, might be useful if some other stuff might be needed as well. ```suggestion authenticated_router.include_router(hitl.router, prefix="/hitl", tags=["Human in the Loop"]) ``` ########## airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py: ########## @@ -0,0 +1,71 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Add Human In the Loop Response table. + +Revision ID: 40f7c30a228b +Revises: ffdb0566c7c0 +Create Date: 2025-07-04 15:05:19.459197 + +""" + +from __future__ import annotations + +import sqlalchemy_jsonfield +from alembic import op +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql + +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + +# revision identifiers, used by Alembic. +revision = "40f7c30a228b" +down_revision = "ffdb0566c7c0" +branch_labels = None +depends_on = None +airflow_version = "3.1.0" + + +def upgrade(): + """Add Human In the Loop Response table.""" + op.create_table( + "hitl_response", + Column( + "ti_id", + String(length=36).with_variant(postgresql.UUID(), "postgresql"), + primary_key=True, + nullable=False, + ), + Column("options", sqlalchemy_jsonfield.JSONField(json=json), nullable=False), + Column("subject", Text, nullable=False), + Column("body", Text, nullable=True), + Column("default", 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("response_at", UtcDateTime, nullable=True), + Column("user_id", String(128), nullable=True), + Column("response_content", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), + Column("params_input", sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}), Review Comment: I'd propose to make this nullable as well. That would signal that there is not input (yet) or never came. ########## airflow-core/src/airflow/models/hitl.py: ########## @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import sqlalchemy_jsonfield +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.hybrid import hybrid_property + +from airflow.models.base import Base +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + + +class HITLResponseModel(Base): + """Human-in-the-loop received response.""" + + __tablename__ = "hitl_response" + ti_id = Column( + String(36).with_variant(postgresql.UUID(as_uuid=False), "postgresql"), + primary_key=True, + nullable=False, + ) + + # Input Request + options = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=False) + subject = Column(Text, nullable=False) + body = Column(Text, nullable=True) + default = 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={}) + + # Response Content Detail + response_at = Column(UtcDateTime, nullable=True) + user_id = Column(String(128), nullable=True) + response_content = Column( + sqlalchemy_jsonfield.JSONField(json=json), + nullable=True, + default=None, + ) + params_input = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}) Review Comment: As comment above: woult propose to have this nullable as well to signal no input (yet) like the three fields above. ```suggestion params_input = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) ``` ########## airflow-core/src/airflow/models/hitl.py: ########## @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import sqlalchemy_jsonfield +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.hybrid import hybrid_property + +from airflow.models.base import Base +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + + +class HITLResponseModel(Base): + """Human-in-the-loop received response.""" + + __tablename__ = "hitl_response" + ti_id = Column( + String(36).with_variant(postgresql.UUID(as_uuid=False), "postgresql"), + primary_key=True, + nullable=False, + ) + + # Input Request Review Comment: Nit: The response from the user is also called "input" So might be a bit of re-working needed ```suggestion # User Request Detail ``` ########## airflow-core/src/airflow/models/hitl.py: ########## @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import sqlalchemy_jsonfield +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.hybrid import hybrid_property + +from airflow.models.base import Base +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + + +class HITLResponseModel(Base): + """Human-in-the-loop received response.""" + + __tablename__ = "hitl_response" + ti_id = Column( + String(36).with_variant(postgresql.UUID(as_uuid=False), "postgresql"), Review Comment: Foreign key to task_instance inlcuding cascaded delete is missing, thereofre it is also represented as standalone table in ERD diagram ########## airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py: ########## @@ -0,0 +1,71 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Add Human In the Loop Response table. + +Revision ID: 40f7c30a228b +Revises: ffdb0566c7c0 +Create Date: 2025-07-04 15:05:19.459197 + +""" + +from __future__ import annotations + +import sqlalchemy_jsonfield +from alembic import op +from sqlalchemy import Boolean, Column, String, Text +from sqlalchemy.dialects import postgresql + +from airflow.settings import json +from airflow.utils.sqlalchemy import UtcDateTime + +# revision identifiers, used by Alembic. +revision = "40f7c30a228b" +down_revision = "ffdb0566c7c0" +branch_labels = None +depends_on = None +airflow_version = "3.1.0" + + +def upgrade(): + """Add Human In the Loop Response table.""" + op.create_table( + "hitl_response", + Column( + "ti_id", Review Comment: Like other comment: Foreign key to task_instance is missing ########## providers/standard/tests/unit/standard/api_fastapi/__init__.py: ########## Review Comment: I assume this file is a leftover from initial PR... can be purged? Asusme API is now only in core and not in provider. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
