|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | # pylint: disable=invalid-name, too-many-lines |
6 | | - |
| 6 | +from __future__ import annotations |
7 | 7 | from datetime import datetime |
8 | 8 | from enum import Enum, IntEnum |
9 | 9 | from uuid import UUID |
|
13 | 13 | import math |
14 | 14 | import os |
15 | 15 |
|
16 | | -from typing import Optional, List, Dict, Union, Literal, Any, get_args |
| 16 | +from typing import Optional, List, Dict, Self, Union, Literal, Any, get_args, get_origin |
17 | 17 | from typing_extensions import Annotated |
18 | 18 |
|
19 | 19 | from pydantic import ( |
|
22 | 22 | HttpUrl as HttpUrlNonStr, |
23 | 23 | AnyHttpUrl as AnyHttpUrlNonStr, |
24 | 24 | EmailStr as CasedEmailStr, |
| 25 | + create_model, |
| 26 | + model_validator, |
25 | 27 | validate_email, |
26 | 28 | RootModel, |
27 | 29 | BeforeValidator, |
@@ -2227,6 +2229,110 @@ class UserOut(UserOutNoId): |
2227 | 2229 | is_superuser: bool = False |
2228 | 2230 |
|
2229 | 2231 |
|
| 2232 | +# ============================================================================ |
| 2233 | +# Feature Flags |
| 2234 | +# ============================================================================ |
| 2235 | + |
| 2236 | + |
| 2237 | +class ValidatedFeatureFlags(BaseModel): |
| 2238 | + """Base class for feature flags with validation.""" |
| 2239 | + |
| 2240 | + @model_validator(mode="after") |
| 2241 | + def validate_all_fields(self) -> Self: |
| 2242 | + """Ensure all fields have descriptions and are bools.""" |
| 2243 | + missing_descriptions = [] |
| 2244 | + non_bool_fields = [] |
| 2245 | + |
| 2246 | + for field_name, field_info in self.model_fields.items(): |
| 2247 | + # Check for missing descriptions |
| 2248 | + if not field_info.description: |
| 2249 | + missing_descriptions.append(field_name) |
| 2250 | + |
| 2251 | + # Check if field type is bool (handles Annotated[bool, ...]) |
| 2252 | + annotation = field_info.annotation |
| 2253 | + if get_origin(annotation) is Annotated: |
| 2254 | + actual_type = get_args(annotation)[0] |
| 2255 | + else: |
| 2256 | + actual_type = annotation |
| 2257 | + |
| 2258 | + if actual_type is not bool: |
| 2259 | + non_bool_fields.append(f"{field_name} (type: {actual_type})") |
| 2260 | + |
| 2261 | + if missing_descriptions: |
| 2262 | + raise ValueError( |
| 2263 | + f"The following fields are missing descriptions: {', '.join(missing_descriptions)}" |
| 2264 | + ) |
| 2265 | + |
| 2266 | + if non_bool_fields: |
| 2267 | + raise ValueError( |
| 2268 | + f"The following fields must be bool type: {', '.join(non_bool_fields)}" |
| 2269 | + ) |
| 2270 | + |
| 2271 | + return self |
| 2272 | + |
| 2273 | + |
| 2274 | +def make_feature_flags_partial(model_cls: type[BaseModel]) -> type[BaseModel]: |
| 2275 | + """Return a partial model for feature flags without validation inheritance. |
| 2276 | +
|
| 2277 | + This creates a model where all fields are optional (bool | None) but doesn't |
| 2278 | + inherit from the original model to avoid the ValidatedFeatureFlags validator |
| 2279 | + that checks for exact bool types. |
| 2280 | + """ |
| 2281 | + new_fields = {} |
| 2282 | + |
| 2283 | + for f_name, f_info in model_cls.model_fields.items(): |
| 2284 | + f_dct = f_info.asdict() # type: ignore |
| 2285 | + |
| 2286 | + # Create a new field that's bool | None with the same description |
| 2287 | + new_fields[f_name] = ( |
| 2288 | + Annotated[ |
| 2289 | + ( |
| 2290 | + bool | None, |
| 2291 | + Field(description=f_dct.get("description"), default=None), # type: ignore |
| 2292 | + ) |
| 2293 | + ], |
| 2294 | + None, |
| 2295 | + ) |
| 2296 | + |
| 2297 | + return create_model( # type: ignore |
| 2298 | + f"{model_cls.__name__}Partial", |
| 2299 | + **new_fields, |
| 2300 | + ) |
| 2301 | + |
| 2302 | + |
| 2303 | +# ============================================================================ |
| 2304 | +# Feature Flags - Edit here |
| 2305 | +# ============================================================================ |
| 2306 | + |
| 2307 | + |
| 2308 | +class FeatureFlags(ValidatedFeatureFlags): |
| 2309 | + """Feature flags for an organization""" |
| 2310 | + |
| 2311 | + dedupeEnabled: bool = Field( |
| 2312 | + description="Enable deduplication options for an org. Intended for beta-testing dedupe.", |
| 2313 | + default=False, |
| 2314 | + ) |
| 2315 | + |
| 2316 | + |
| 2317 | +# ============================================================================ |
| 2318 | + |
| 2319 | + |
| 2320 | +FeatureFlagsPartial = make_feature_flags_partial(FeatureFlags) |
| 2321 | + |
| 2322 | + |
| 2323 | +class FeatureFlagStats(BaseModel): |
| 2324 | + """Output model for feature flags""" |
| 2325 | + |
| 2326 | + model_config = ConfigDict(use_attribute_docstrings=True) |
| 2327 | + |
| 2328 | + name: str |
| 2329 | + |
| 2330 | + description: str |
| 2331 | + |
| 2332 | + count: int |
| 2333 | + """Number of organizations that have this feature flag enabled.""" |
| 2334 | + |
| 2335 | + |
2230 | 2336 | # ============================================================================ |
2231 | 2337 | # ORGS |
2232 | 2338 | # ============================================================================ |
@@ -2354,6 +2460,8 @@ class OrgOut(BaseMongoModel): |
2354 | 2460 | publicDescription: str = "" |
2355 | 2461 | publicUrl: str = "" |
2356 | 2462 |
|
| 2463 | + featureFlags: FeatureFlags = FeatureFlags() |
| 2464 | + |
2357 | 2465 |
|
2358 | 2466 | # ============================================================================ |
2359 | 2467 | class Organization(BaseMongoModel): |
@@ -2417,6 +2525,8 @@ class Organization(BaseMongoModel): |
2417 | 2525 | publicDescription: Optional[str] = None |
2418 | 2526 | publicUrl: Optional[str] = None |
2419 | 2527 |
|
| 2528 | + featureFlags: FeatureFlags = FeatureFlags() |
| 2529 | + |
2420 | 2530 | def is_owner(self, user): |
2421 | 2531 | """Check if user is owner""" |
2422 | 2532 | return self._is_auth(user, UserRole.OWNER) |
|
0 commit comments