Coverage for /home/airflow/.local/lib/python3.12/site-packages/airflow/api_fastapi/core_api/datamodels/variables.py: 96%
51 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 14:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 14:22 +0000
1# Licensed to the Apache Software Foundation (ASF) under one
2# or more contributor license agreements. See the NOTICE file
3# distributed with this work for additional information
4# regarding copyright ownership. The ASF licenses this file
5# to you under the Apache License, Version 2.0 (the
6# "License"); you may not use this file except in compliance
7# with the License. You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing,
12# software distributed under the License is distributed on an
13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14# KIND, either express or implied. See the License for the
15# specific language governing permissions and limitations
16# under the License.
18from __future__ import annotations
20import json
21from collections.abc import Iterable
23from pydantic import Field, JsonValue, field_validator, model_validator
25from airflow._shared.secrets_masker import redact
26from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model
27from airflow.configuration import conf
28from airflow.models.base import ID_LEN
29from airflow.typing_compat import Self
32class VariableResponse(BaseModel):
33 """Variable serializer for responses."""
35 key: str
36 val: str | None = Field(alias="value", default=None)
37 description: str | None
38 is_encrypted: bool
39 team_name: str | None
41 @model_validator(mode="after")
42 def redact_val(self) -> Self:
43 if self.val is None: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true
44 return self
45 try:
46 val_dict = json.loads(self.val)
47 redacted_dict = redact(val_dict, self.key)
48 self.val = json.dumps(redacted_dict)
49 return self
50 except json.JSONDecodeError:
51 # value is not a serialized string representation of a dict.
52 self.val = str(redact(self.val, self.key))
53 return self
56class VariableBody(StrictBaseModel):
57 """Variable serializer for bodies."""
59 key: str = Field(max_length=ID_LEN)
60 value: JsonValue = Field(serialization_alias="val")
61 description: str | None = Field(default=None)
62 team_name: str | None = Field(max_length=50, default=None)
64 @field_validator("value", mode="after")
65 @classmethod
66 def serialize_non_string_value(cls, value: JsonValue) -> str:
67 # Variables are stored as strings. A non-string JSON value (list, dict, number, bool,
68 # null) must be JSON-encoded here: POST would otherwise store its Python repr, which
69 # deserialize_json cannot read back, and PATCH would fail in the ORM Fernet step on
70 # bytes(value, "utf-8") (see #71010). indent=2 matches Variable.set(serialize_json=True).
71 # Not allow_nan=False: rejecting here yields a 422 whose response embeds the NaN input,
72 # which starlette's JSONResponse cannot serialize -- the request would 500 instead.
73 if isinstance(value, str):
74 return value
75 return json.dumps(value, indent=2)
77 @model_validator(mode="after")
78 def validate_team_name(self) -> VariableBody:
79 if self.team_name is not None and not conf.getboolean("core", "multi_team"):
80 raise ValueError(
81 "team_name cannot be set when multi_team mode is disabled. Please contact your administrator."
82 )
83 return self
86VariableBodyPartial = make_partial_model(VariableBody)
89class VariableCollectionResponse(BaseModel):
90 """Variable Collection serializer for responses."""
92 variables: Iterable[VariableResponse]
93 total_entries: int
96class VariablesImportResponse(BaseModel):
97 """Import Variables serializer for responses."""
99 created_variable_keys: list[str]
100 import_count: int
101 created_count: int