diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index 0e56102788729..cf940c52c9cc1 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -430,7 +430,7 @@ def update( """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) @@ -652,7 +652,9 @@ def delete(self, pool: str) -> str | ServerResponseError: 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) @@ -766,7 +768,9 @@ def delete(self, variable_key: str) -> str | ServerResponseError: 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 1bc183d4f4592..4a889cb2b4bf6 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py @@ -877,14 +877,7 @@ def handle_request(request: httpx.Request) -> httpx.Response: 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( @@ -895,6 +888,30 @@ def handle_request(request: httpx.Request) -> httpx.Response: 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, @@ -1709,6 +1726,19 @@ def handle_request(request: httpx.Request) -> httpx.Response: 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( @@ -1978,6 +2008,18 @@ def handle_request(request: httpx.Request) -> httpx.Response: 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(