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

ash pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 893f6279e6b Allow slash in Variable keys from TaskSDK 
read/write(#55324)
893f6279e6b is described below

commit 893f6279e6bb68ed875b2959300f26a58dc98a4a
Author: Duc Nguyen <[email protected]>
AuthorDate: Sun Sep 14 00:49:13 2025 +0700

    Allow slash in Variable keys from TaskSDK read/write(#55324)
---
 .../api_fastapi/execution_api/routes/variables.py  | 15 ++++-
 .../execution_api/versions/head/test_variables.py  | 75 +++++++++++++++-------
 2 files changed, 65 insertions(+), 25 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py
index e1685987866..d2f5d21349c 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py
@@ -55,7 +55,7 @@ log = logging.getLogger(__name__)
 
 
 @router.get(
-    "/{variable_key}",
+    "/{variable_key:path}",
     responses={
         status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"},
         status.HTTP_403_FORBIDDEN: {"description": "Task does not have access 
to the variable"},
@@ -63,6 +63,9 @@ log = logging.getLogger(__name__)
 )
 def get_variable(variable_key: str) -> VariableResponse:
     """Get an Airflow Variable."""
+    if not variable_key:
+        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Not Found")
+
     try:
         variable_value = Variable.get(variable_key)
     except KeyError:
@@ -78,7 +81,7 @@ def get_variable(variable_key: str) -> VariableResponse:
 
 
 @router.put(
-    "/{variable_key}",
+    "/{variable_key:path}",
     status_code=status.HTTP_201_CREATED,
     responses={
         status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"},
@@ -87,12 +90,15 @@ def get_variable(variable_key: str) -> VariableResponse:
 )
 def put_variable(variable_key: str, body: VariablePostBody):
     """Set an Airflow Variable."""
+    if not variable_key:
+        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Not Found")
+
     Variable.set(key=variable_key, value=body.value, 
description=body.description)
     return {"message": "Variable successfully set"}
 
 
 @router.delete(
-    "/{variable_key}",
+    "/{variable_key:path}",
     status_code=status.HTTP_204_NO_CONTENT,
     responses={
         status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"},
@@ -101,4 +107,7 @@ def put_variable(variable_key: str, body: VariablePostBody):
 )
 def delete_variable(variable_key: str):
     """Delete an Airflow Variable."""
+    if not variable_key:
+        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Not Found")
+
     Variable.delete(key=variable_key)
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py
index 0ada165ed5b..0e112ad330a 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py
@@ -65,17 +65,24 @@ def access_denied(client):
 
 
 class TestGetVariable:
-    def test_variable_get_from_db(self, client, session):
-        Variable.set(key="var1", value="value", session=session)
+    @pytest.mark.parametrize(
+        "key, value",
+        [
+            ("var1", "value"),
+            ("var2/with_slash", "slash_value"),
+        ],
+    )
+    def test_variable_get_from_db(self, client, session, key, value):
+        Variable.set(key=key, value=value, session=session)
         session.commit()
 
-        response = client.get("/execution/variables/var1")
+        response = client.get(f"/execution/variables/{key}")
 
         assert response.status_code == 200
-        assert response.json() == {"key": "var1", "value": "value"}
+        assert response.json() == {"key": key, "value": value}
 
         # Remove connection
-        Variable.delete(key="var1", session=session)
+        Variable.delete(key=key, session=session)
         session.commit()
 
     @mock.patch.dict(
@@ -88,13 +95,20 @@ class TestGetVariable:
         assert response.status_code == 200
         assert response.json() == {"key": "key1", "value": "VALUE"}
 
-    def test_variable_get_not_found(self, client):
-        response = client.get("/execution/variables/non_existent_var")
+    @pytest.mark.parametrize(
+        "key",
+        [
+            "non_existent_var",
+            "non/existent/slash/var",
+        ],
+    )
+    def test_variable_get_not_found(self, client, key):
+        response = client.get(f"/execution/variables/{key}")
 
         assert response.status_code == 404
         assert response.json() == {
             "detail": {
-                "message": "Variable with key 'non_existent_var' not found",
+                "message": f"Variable with key '{key}' not found",
                 "reason": "not_found",
             }
         }
@@ -117,14 +131,18 @@ class TestGetVariable:
 
 class TestPutVariable:
     @pytest.mark.parametrize(
-        "payload",
+        "key, payload",
         [
-            pytest.param({"value": "{}", "description": "description"}, 
id="valid-payload"),
-            pytest.param({"value": "{}"}, id="missing-description"),
+            pytest.param("var_create", {"value": "{}", "description": 
"description"}, id="valid-payload"),
+            pytest.param("var_create", {"value": "{}"}, 
id="missing-description"),
+            pytest.param(
+                "var_create/with_slash",
+                {"value": "slash_value", "description": "Variable with slash"},
+                id="slash-key",
+            ),
         ],
     )
-    def test_should_create_variable(self, client, payload, session):
-        key = "var_create"
+    def test_should_create_variable(self, client, key, payload, session):
         response = client.put(
             f"/execution/variables/{key}",
             json=payload,
@@ -132,7 +150,7 @@ class TestPutVariable:
         assert response.status_code == 201, response.json()
         assert response.json()["message"] == "Variable successfully set"
 
-        var_from_db = session.query(Variable).where(Variable.key == 
"var_create").first()
+        var_from_db = session.query(Variable).where(Variable.key == 
key).first()
         assert var_from_db is not None
         assert var_from_db.key == key
         assert var_from_db.val == payload["value"]
@@ -179,8 +197,14 @@ class TestPutVariable:
         assert response.json()["detail"][0]["type"] == "extra_forbidden"
         assert response.json()["detail"][0]["msg"] == "Extra inputs are not 
permitted"
 
-    def test_overwriting_existing_variable(self, client, session):
-        key = "var_create"
+    @pytest.mark.parametrize(
+        "key",
+        [
+            "var_create",
+            "var_create/with_slash",
+        ],
+    )
+    def test_overwriting_existing_variable(self, client, session, key):
         Variable.set(key=key, value="value", session=session)
         session.commit()
 
@@ -218,19 +242,26 @@ class TestPutVariable:
 
 
 class TestDeleteVariable:
-    def test_should_delete_variable(self, client, session):
-        for i in range(1, 3):
-            Variable.set(key=f"key{i}", value=i)
+    @pytest.mark.parametrize(
+        "keys_to_create, key_to_delete",
+        [
+            (["key1", "key2"], "key1"),
+            (["key3/with_slash", "key4"], "key3/with_slash"),
+        ],
+    )
+    def test_should_delete_variable(self, client, session, keys_to_create, 
key_to_delete):
+        for i, key in enumerate(keys_to_create, 1):
+            Variable.set(key=key, value=str(i))
 
         vars = session.query(Variable).all()
-        assert len(vars) == 2
+        assert len(vars) == len(keys_to_create)
 
-        response = client.delete("/execution/variables/key1")
+        response = client.delete(f"/execution/variables/{key_to_delete}")
 
         assert response.status_code == 204
 
         vars = session.query(Variable).all()
-        assert len(vars) == 1
+        assert len(vars) == len(keys_to_create) - 1
 
     def test_should_not_delete_variable(self, client, session):
         Variable.set(key="key", value="value")

Reply via email to