This is an automated email from the ASF dual-hosted git repository.
henry3260 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 350ae2e10ac Stop airflowctl update commands from clearing fields left
unset (#71333)
350ae2e10ac is described below
commit 350ae2e10ac184113fa0aa08e72e72c672803e2a
Author: Devansh Bhensdadia <[email protected]>
AuthorDate: Sun Sep 13 23:26:49 2026 +0530
Stop airflowctl update commands from clearing fields left unset (#71333)
---
airflow-ctl/src/airflowctl/api/operations.py | 10 ++--
.../tests/airflow_ctl/api/test_operations.py | 56 +++++++++++++++++++---
2 files changed, 56 insertions(+), 10 deletions(-)
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index a800fa5390c..17f72f2d5d4 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -465,7 +465,7 @@ class ConnectionsOperations(BaseOperations):
"""Update a connection."""
self.response = self.client.patch(
f"connections/{connection.connection_id}",
- json=connection.model_dump(mode="json", by_alias=True),
+ json=connection.model_dump(mode="json", by_alias=True,
exclude_none=True),
)
return ConnectionResponse.model_validate_json(self.response.content)
@@ -687,7 +687,9 @@ class PoolsOperations(BaseOperations):
def update(self, pool_body: PoolPatchBody) -> PoolResponse |
ServerResponseError:
"""Update a pool."""
- self.response = self.client.patch(f"pools/{pool_body.pool}",
json=pool_body.model_dump(mode="json"))
+ self.response = self.client.patch(
+ f"pools/{pool_body.pool}", json=pool_body.model_dump(mode="json",
exclude_none=True)
+ )
return PoolResponse.model_validate_json(self.response.content)
@@ -801,7 +803,9 @@ class VariablesOperations(BaseOperations):
def update(self, variable: VariableBody) -> VariableResponse |
ServerResponseError:
"""Update a variable."""
- self.response = self.client.patch(f"variables/{variable.key}",
json=variable.model_dump(mode="json"))
+ self.response = self.client.patch(
+ f"variables/{variable.key}", json=variable.model_dump(mode="json",
exclude_none=True)
+ )
return VariableResponse.model_validate_json(self.response.content)
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 6e52ba1a049..583c6ff2342 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -968,14 +968,7 @@ class TestConnectionsOperations:
assert request_body == {
"connection_id": self.connection_id,
"conn_type": self.conn_type,
- "description": None,
- "host": None,
- "login": None,
"schema": self.schema_,
- "port": None,
- "password": None,
- "extra": None,
- "team_name": None,
}
assert "schema_" not in request_body
return httpx.Response(
@@ -986,6 +979,30 @@ class TestConnectionsOperations:
response = client.connections.update(connection=connection)
assert response == self.connection_response
+ def test_update_omits_unset_fields_from_request_body(self):
+ # The API treats every key present in a PATCH body as an intentional
value, so sending
+ # unset fields as null clears the stored login, port, schema and
description.
+ connection = ConnectionBody(
+ connection_id=self.connection_id,
+ conn_type=self.conn_type,
+ host="new-host",
+ )
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path ==
f"/api/v2/connections/{self.connection_id}"
+ assert json.loads(request.content.decode()) == {
+ "connection_id": self.connection_id,
+ "conn_type": self.conn_type,
+ "host": "new-host",
+ }
+ return httpx.Response(
+ 200,
json=json.loads(self.connection_response.model_dump_json(by_alias=True))
+ )
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.connections.update(connection=connection)
+ assert response == self.connection_response
+
def test_test(self):
connection_test_response = ConnectionTestResponse(
status=True,
@@ -1800,6 +1817,19 @@ class TestPoolsOperations:
response = client.pools.update(pool_body=self.pool_patch_body)
assert response == self.pool_response
+ def test_update_omits_unset_fields_from_request_body(self):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path == f"/api/v2/pools/{self.pool_name}"
+ assert json.loads(request.content.decode()) == {
+ "pool": self.pool_name,
+ "description": "description",
+ }
+ return httpx.Response(200,
json=json.loads(self.pool_response.model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.pools.update(pool_body=self.pool_patch_body)
+ assert response == self.pool_response
+
class TestProvidersOperations:
provider_response = ProviderResponse(
@@ -2069,6 +2099,18 @@ class TestVariablesOperations:
response = client.variables.update(variable=self.variable)
assert response == self.variable_response
+ def test_update_omits_unset_fields_from_request_body(self):
+ variable = VariableBody.model_validate({"key": self.key, "value":
"new-value"})
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path == f"/api/v2/variables/{self.key}"
+ assert json.loads(request.content.decode()) == {"key": self.key,
"value": "new-value"}
+ return httpx.Response(200,
json=json.loads(self.variable_response.model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.variables.update(variable=variable)
+ assert response == self.variable_response
+
class TestVersionOperations:
version_info = VersionInfo(