Coverage for /home/airflow/.local/lib/python3.12/site-packages/airflow/api_fastapi/common/types.py: 60%
117 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.
17from __future__ import annotations
19import re
20from dataclasses import dataclass
21from datetime import timedelta
22from enum import Enum
23from typing import Annotated, Literal
25from pydantic import (
26 AfterValidator,
27 AliasGenerator,
28 AwareDatetime,
29 BaseModel,
30 BeforeValidator,
31 ConfigDict,
32 field_validator,
33 model_serializer,
34 model_validator,
35)
37from airflow._shared.timezones import timezone
39UtcDateTime = Annotated[AwareDatetime, AfterValidator(lambda d: d.astimezone(timezone.utc))]
40"""UTCDateTime is a datetime with timezone information"""
43def _validate_timedelta_field(td: timedelta | None) -> TimeDelta | None:
44 """Validate the timedelta field and return it."""
45 if td is None: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true
46 return None
47 return TimeDelta(
48 days=td.days,
49 seconds=td.seconds,
50 microseconds=td.microseconds,
51 )
54class TimeDelta(BaseModel):
55 """TimeDelta can be used to interact with datetime.timedelta objects."""
57 object_type: str = "TimeDelta"
58 days: int
59 seconds: int
60 microseconds: int
62 model_config = ConfigDict(
63 alias_generator=AliasGenerator(
64 serialization_alias=lambda field_name: {
65 "object_type": "__type",
66 }.get(field_name, field_name),
67 )
68 )
71TimeDeltaWithValidation = Annotated[TimeDelta, BeforeValidator(_validate_timedelta_field)]
74# Common validator for theme icon fields (SVG-only, http(s) or app-relative path).
75def _validate_theme_icon(value: str | None) -> str | None:
76 if value is None:
77 return value
78 from urllib.parse import urlparse
80 parsed = urlparse(value)
81 if parsed.scheme in ("http", "https"):
82 path = parsed.path or ""
83 elif parsed.scheme == "" and value.startswith("/"):
84 path = value
85 else:
86 raise ValueError("theme.icon must be http(s) URL or app-relative path starting with '/'")
87 if not path.lower().endswith(".svg"):
88 raise ValueError("theme.icon must point to an SVG file (*.svg)")
89 return value
92# Alias type for theme icon fields with shared validation
93ThemeIconType = Annotated[str | None, BeforeValidator(_validate_theme_icon)]
96class Mimetype(str, Enum):
97 """Mimetype for the `Content-Type` header."""
99 TEXT = "text/plain"
100 JSON = "application/json"
101 FORM = "application/x-www-form-urlencoded"
102 NDJSON = "application/x-ndjson"
103 ANY = "*/*"
106@dataclass
107class ExtraMenuItem:
108 """Define a menu item that can be added to the menu by auth managers or plugins."""
110 text: str
111 href: str
114class MenuItem(Enum):
115 """Define all menu items defined in the menu."""
117 REQUIRED_ACTIONS = "Required Actions"
118 ASSETS = "Assets"
119 AUDIT_LOG = "Audit Log"
120 CONFIG = "Config"
121 CONNECTIONS = "Connections"
122 DAGS = "Dags"
123 DEADLINES = "Deadlines"
124 DOCS = "Docs"
125 JOBS = "Jobs"
126 PLUGINS = "Plugins"
127 POOLS = "Pools"
128 PROVIDERS = "Providers"
129 VARIABLES = "Variables"
130 XCOMS = "XComs"
133class UIAlert(BaseModel):
134 """Optional alert to be shown at the top of the page."""
136 text: str
137 category: Literal["info", "warning", "error"]
140class OklchColor(BaseModel):
141 """Validates OKLCH color format from string oklch(l c h)."""
143 lightness: float
144 chroma: float
145 hue: float
147 @model_validator(mode="before")
148 @classmethod
149 def parse_oklch_string(cls, data):
150 if isinstance(data, str):
151 oklch_regex_pattern = r"^oklch\((-?\d+(?:\.\d+)?) (-?\d+(?:\.\d+)?) (-?\d+(?:\.\d+)?)\)$"
152 match = re.match(oklch_regex_pattern, data)
154 if not match:
155 raise ValueError(f"Invalid OKLCH format: {data} Expected format oklch(l c h)")
157 ligthness_str, chroma_str, hue_str = match.groups()
159 return {
160 "lightness": float(ligthness_str),
161 "chroma": float(chroma_str),
162 "hue": float(hue_str),
163 }
164 return data
166 @field_validator("lightness")
167 @classmethod
168 def validate_lightness(cls, value: float) -> float:
169 if value < 0 or value > 1:
170 raise ValueError(f"Invalid lightness: {value} Must be between 0 and 1")
171 return value
173 @field_validator("chroma")
174 @classmethod
175 def validate_chroma(cls, value: float) -> float:
176 if value < 0 or value > 0.5:
177 raise ValueError(f"Invalid chroma: {value} Must be between 0 and 0.5")
178 return value
180 @field_validator("hue")
181 @classmethod
182 def validate_hue(cls, value: float) -> float:
183 if value < 0 or value > 360:
184 raise ValueError(f"Invalid hue: {value} Must be between 0 and 360")
185 return value
187 @model_serializer(mode="plain")
188 def serialize_model(self) -> str:
189 return f"oklch({self.lightness} {self.chroma} {self.hue})"
192# Private type aliases for theme token shapes
193_ColorShade = dict[Literal["value"], OklchColor]
194_SHADE_LITERAL = Literal["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"]
195_ColorScale = dict[_SHADE_LITERAL, _ColorShade]
198class ThemeColors(BaseModel):
199 """Color tokens for the UI theme. All fields are optional; at least one must be provided."""
201 brand: _ColorScale | None = None
202 gray: _ColorScale | None = None
203 black: _ColorShade | None = None
204 white: _ColorShade | None = None
206 @model_validator(mode="after")
207 def check_at_least_one_color(self) -> ThemeColors:
208 if not any([self.brand, self.gray, self.black, self.white]):
209 raise ValueError("At least one color token must be provided: brand, gray, black, or white")
210 return self
213class Theme(BaseModel):
214 """JSON to modify Chakra's theme."""
216 tokens: dict[Literal["colors"], ThemeColors] | None = None
217 globalCss: dict[str, dict] | None = None
218 icon: ThemeIconType = None
219 icon_dark_mode: ThemeIconType = None