teoat commited on
Commit
caf7f7e
·
verified ·
1 Parent(s): c8b720c

Upload core/feature_flags_legacy.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. core/feature_flags_legacy.py +433 -0
core/feature_flags_legacy.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feature Flag Management System
3
+ Enables gradual rollout of features with granular control
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Set
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class FlagType(Enum):
18
+ """Types of feature flags"""
19
+
20
+ BOOLEAN = "boolean"
21
+ PERCENTAGE = "percentage"
22
+ ALLOWLIST = "allowlist"
23
+ BLOCKLIST = "blocklist"
24
+
25
+
26
+ class FlagStatus(Enum):
27
+ """Feature flag status"""
28
+
29
+ ENABLED = "enabled"
30
+ DISABLED = "disabled"
31
+ ROLLING_OUT = "rolling_out"
32
+ ROLLING_BACK = "rolling_back"
33
+
34
+
35
+ @dataclass
36
+ class FeatureFlag:
37
+ """Feature flag configuration"""
38
+
39
+ key: str
40
+ name: str
41
+ description: str
42
+ flag_type: str
43
+ status: str
44
+ value: Any
45
+ created_at: str
46
+ updated_at: str
47
+ created_by: str
48
+ tags: List[str] = field(default_factory=list)
49
+ metadata: Dict[str, Any] = field(default_factory=dict)
50
+ rollout_percentage: Optional[int] = None
51
+ allowlist: Set[str] = field(default_factory=set)
52
+ blocklist: Set[str] = field(default_factory=set)
53
+ expires_at: Optional[str] = None
54
+
55
+
56
+ class FeatureFlagManager:
57
+ """Feature flag management system"""
58
+
59
+ def __init__(self, config_path: Path = Path("config/feature_flags.json")):
60
+ self.config_path = config_path
61
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
62
+
63
+ self.flags: Dict[str, FeatureFlag] = {}
64
+ self.audit_log: List[Dict[str, Any]] = []
65
+ self.load_flags()
66
+
67
+ def load_flags(self):
68
+ """Load feature flags from configuration"""
69
+ if not self.config_path.exists():
70
+ self._initialize_default_flags()
71
+ return
72
+
73
+ try:
74
+ with open(self.config_path, "r") as f:
75
+ data = json.load(f)
76
+
77
+ for key, flag_data in data.get("flags", {}).items():
78
+ flag = FeatureFlag(
79
+ key=key,
80
+ name=flag_data["name"],
81
+ description=flag_data["description"],
82
+ flag_type=flag_data["flag_type"],
83
+ status=flag_data["status"],
84
+ value=flag_data["value"],
85
+ created_at=flag_data["created_at"],
86
+ updated_at=flag_data["updated_at"],
87
+ created_by=flag_data.get("created_by", "system"),
88
+ tags=flag_data.get("tags", []),
89
+ metadata=flag_data.get("metadata", {}),
90
+ rollout_percentage=flag_data.get("rollout_percentage"),
91
+ allowlist=set(flag_data.get("allowlist", [])),
92
+ blocklist=set(flag_data.get("blocklist", [])),
93
+ expires_at=flag_data.get("expires_at"),
94
+ )
95
+ self.flags[key] = flag
96
+
97
+ logger.info(f"Loaded {len(self.flags)} feature flags")
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error loading feature flags: {e}")
101
+ self._initialize_default_flags()
102
+
103
+ def _initialize_default_flags(self):
104
+ """Initialize with default feature flags"""
105
+ default_flags = {
106
+ "new_dashboard_ui": FeatureFlag(
107
+ key="new_dashboard_ui",
108
+ name="New Dashboard UI",
109
+ description="Enable redesigned dashboard interface",
110
+ flag_type=FlagType.BOOLEAN.value,
111
+ status=FlagStatus.DISABLED.value,
112
+ value=False,
113
+ created_at=datetime.now().isoformat(),
114
+ updated_at=datetime.now().isoformat(),
115
+ created_by="system",
116
+ tags=["ui", "dashboard"],
117
+ ),
118
+ "advanced_ai_analysis": FeatureFlag(
119
+ key="advanced_ai_analysis",
120
+ name="Advanced AI Analysis",
121
+ description="Enable advanced AI-powered analysis features",
122
+ flag_type=FlagType.PERCENTAGE.value,
123
+ status=FlagStatus.ROLLING_OUT.value,
124
+ value=True,
125
+ created_at=datetime.now().isoformat(),
126
+ updated_at=datetime.now().isoformat(),
127
+ created_by="system",
128
+ tags=["ai", "analysis"],
129
+ rollout_percentage=20,
130
+ ),
131
+ "simplified_workflow": FeatureFlag(
132
+ key="simplified_workflow",
133
+ name="Simplified Workflow",
134
+ description="Simplified investigation workflow for new users",
135
+ flag_type=FlagType.BOOLEAN.value,
136
+ status=FlagStatus.ENABLED.value,
137
+ value=True,
138
+ created_at=datetime.now().isoformat(),
139
+ updated_at=datetime.now().isoformat(),
140
+ created_by="system",
141
+ tags=["workflow", "ux"],
142
+ ),
143
+ "beta_reporting": FeatureFlag(
144
+ key="beta_reporting",
145
+ name="Beta Reporting",
146
+ description="Enable new reporting features in beta",
147
+ flag_type=FlagType.ALLOWLIST.value,
148
+ status=FlagStatus.ROLLING_OUT.value,
149
+ value=True,
150
+ created_at=datetime.now().isoformat(),
151
+ updated_at=datetime.now().isoformat(),
152
+ created_by="system",
153
+ tags=["reporting", "beta"],
154
+ allowlist={"admin@company.com", "beta-testers@company.com"},
155
+ ),
156
+ }
157
+
158
+ self.flags = default_flags
159
+ self.save_flags()
160
+
161
+ def save_flags(self):
162
+ """Save feature flags to configuration"""
163
+ data = {
164
+ "version": "1.0",
165
+ "last_updated": datetime.now().isoformat(),
166
+ "flags": {},
167
+ }
168
+
169
+ for key, flag in self.flags.items():
170
+ data["flags"][key] = {
171
+ "name": flag.name,
172
+ "description": flag.description,
173
+ "flag_type": flag.flag_type,
174
+ "status": flag.status,
175
+ "value": flag.value,
176
+ "created_at": flag.created_at,
177
+ "updated_at": flag.updated_at,
178
+ "created_by": flag.created_by,
179
+ "tags": flag.tags,
180
+ "metadata": flag.metadata,
181
+ "rollout_percentage": flag.rollout_percentage,
182
+ "allowlist": list(flag.allowlist),
183
+ "blocklist": list(flag.blocklist),
184
+ "expires_at": flag.expires_at,
185
+ }
186
+
187
+ with open(self.config_path, "w") as f:
188
+ json.dump(data, f, indent=2)
189
+
190
+ logger.info(f"Saved {len(self.flags)} feature flags")
191
+
192
+ def is_enabled(
193
+ self,
194
+ key: str,
195
+ user_id: Optional[str] = None,
196
+ context: Optional[Dict[str, Any]] = None,
197
+ ) -> bool:
198
+ """Check if feature flag is enabled for given user/context"""
199
+ if key not in self.flags:
200
+ logger.warning(f"Unknown feature flag: {key}")
201
+ return False
202
+
203
+ flag = self.flags[key]
204
+
205
+ if flag.status == FlagStatus.DISABLED.value:
206
+ return False
207
+
208
+ if flag.expires_at:
209
+ expires = datetime.fromisoformat(flag.expires_at)
210
+ if datetime.now() > expires:
211
+ logger.warning(f"Feature flag {key} has expired")
212
+ return False
213
+
214
+ if flag.flag_type == FlagType.BOOLEAN.value:
215
+ return flag.value
216
+
217
+ elif flag.flag_type == FlagType.PERCENTAGE.value:
218
+ if not flag.rollout_percentage or flag.rollout_percentage >= 100:
219
+ return True
220
+
221
+ if user_id and context:
222
+ hash_value = hash(f"{key}_{user_id}_{context.get('tenant_id', '')}")
223
+ rollout = hash_value % 100
224
+ return rollout < flag.rollout_percentage
225
+
226
+ return flag.rollout_percentage > 0
227
+
228
+ elif flag.flag_type == FlagType.ALLOWLIST.value:
229
+ if user_id:
230
+ return user_id in flag.allowlist
231
+ return False
232
+
233
+ elif flag.flag_type == FlagType.BLOCKLIST.value:
234
+ if user_id:
235
+ return user_id not in flag.blocklist
236
+ return True
237
+
238
+ return flag.value
239
+
240
+ def create_flag(
241
+ self,
242
+ key: str,
243
+ name: str,
244
+ description: str,
245
+ flag_type: str,
246
+ value: Any = False,
247
+ created_by: str = "system",
248
+ tags: Optional[List[str]] = None,
249
+ metadata: Optional[Dict[str, Any]] = None,
250
+ ) -> bool:
251
+ """Create new feature flag"""
252
+ if key in self.flags:
253
+ logger.error(f"Feature flag {key} already exists")
254
+ return False
255
+
256
+ flag = FeatureFlag(
257
+ key=key,
258
+ name=name,
259
+ description=description,
260
+ flag_type=flag_type,
261
+ status=FlagStatus.ENABLED.value if value else FlagStatus.DISABLED.value,
262
+ value=value,
263
+ created_at=datetime.now().isoformat(),
264
+ updated_at=datetime.now().isoformat(),
265
+ created_by=created_by,
266
+ tags=tags or [],
267
+ metadata=metadata or {},
268
+ )
269
+
270
+ self.flags[key] = flag
271
+ self.save_flags()
272
+
273
+ self._log_audit("CREATE", key, created_by, {"name": name, "value": value})
274
+
275
+ logger.info(f"Created feature flag: {key}")
276
+ return True
277
+
278
+ def update_flag(
279
+ self,
280
+ key: str,
281
+ value: Optional[Any] = None,
282
+ status: Optional[str] = None,
283
+ rollout_percentage: Optional[int] = None,
284
+ allowlist: Optional[Set[str]] = None,
285
+ blocklist: Optional[Set[str]] = None,
286
+ updated_by: str = "system",
287
+ ) -> bool:
288
+ """Update existing feature flag"""
289
+ if key not in self.flags:
290
+ logger.error(f"Feature flag {key} does not exist")
291
+ return False
292
+
293
+ flag = self.flags[key]
294
+ changes = {}
295
+
296
+ if value is not None:
297
+ changes["value"] = {"old": flag.value, "new": value}
298
+ flag.value = value
299
+
300
+ if status is not None:
301
+ changes["status"] = {"old": flag.status, "new": status}
302
+ flag.status = status
303
+
304
+ if rollout_percentage is not None:
305
+ changes["rollout_percentage"] = {
306
+ "old": flag.rollout_percentage,
307
+ "new": rollout_percentage,
308
+ }
309
+ flag.rollout_percentage = rollout_percentage
310
+
311
+ if allowlist is not None:
312
+ changes["allowlist"] = {"old": list(flag.allowlist), "new": list(allowlist)}
313
+ flag.allowlist = allowlist
314
+
315
+ if blocklist is not None:
316
+ changes["blocklist"] = {"old": list(flag.blocklist), "new": list(blocklist)}
317
+ flag.blocklist = blocklist
318
+
319
+ flag.updated_at = datetime.now().isoformat()
320
+ self.save_flags()
321
+
322
+ self._log_audit("UPDATE", key, updated_by, changes)
323
+
324
+ logger.info(f"Updated feature flag: {key}")
325
+ return True
326
+
327
+ def delete_flag(self, key: str, deleted_by: str = "system") -> bool:
328
+ """Delete feature flag"""
329
+ if key not in self.flags:
330
+ logger.error(f"Feature flag {key} does not exist")
331
+ return False
332
+
333
+ flag = self.flags[key]
334
+ del self.flags[key]
335
+
336
+ self.save_flags()
337
+
338
+ self._log_audit("DELETE", key, deleted_by, {"flag_data": flag.name})
339
+
340
+ logger.info(f"Deleted feature flag: {key}")
341
+ return True
342
+
343
+ def _log_audit(self, action: str, key: str, actor: str, details: Dict[str, Any]):
344
+ """Log audit entry"""
345
+ entry = {
346
+ "timestamp": datetime.now().isoformat(),
347
+ "action": action,
348
+ "flag_key": key,
349
+ "actor": actor,
350
+ "details": details,
351
+ }
352
+ self.audit_log.append(entry)
353
+
354
+ audit_path = self.config_path.parent / "feature_flag_audit.json"
355
+ with open(audit_path, "w") as f:
356
+ json.dump(self.audit_log[-100:], f, indent=2)
357
+
358
+ def get_flags_by_tag(self, tag: str) -> List[FeatureFlag]:
359
+ """Get all flags with specific tag"""
360
+ return [flag for flag in self.flags.values() if tag in flag.tags]
361
+
362
+ def get_expired_flags(self) -> List[FeatureFlag]:
363
+ """Get all expired flags"""
364
+ expired = []
365
+ now = datetime.now()
366
+
367
+ for flag in self.flags.values():
368
+ if flag.expires_at:
369
+ expires = datetime.fromisoformat(flag.expires_at)
370
+ if now > expires:
371
+ expired.append(flag)
372
+
373
+ return expired
374
+
375
+ def generate_report(self) -> Dict[str, Any]:
376
+ """Generate feature flag report"""
377
+ report = {
378
+ "generated_at": datetime.now().isoformat(),
379
+ "summary": {
380
+ "total_flags": len(self.flags),
381
+ "enabled_flags": sum(
382
+ 1
383
+ for f in self.flags.values()
384
+ if f.status == FlagStatus.ENABLED.value
385
+ ),
386
+ "disabled_flags": sum(
387
+ 1
388
+ for f in self.flags.values()
389
+ if f.status == FlagStatus.DISABLED.value
390
+ ),
391
+ "rolling_out_flags": sum(
392
+ 1
393
+ for f in self.flags.values()
394
+ if f.status == FlagStatus.ROLLING_OUT.value
395
+ ),
396
+ "rolling_back_flags": sum(
397
+ 1
398
+ for f in self.flags.values()
399
+ if f.status == FlagStatus.ROLLING_BACK.value
400
+ ),
401
+ "expired_flags": len(self.get_expired_flags()),
402
+ },
403
+ "flags": {},
404
+ "tags": {},
405
+ }
406
+
407
+ for key, flag in self.flags.items():
408
+ report["flags"][key] = {
409
+ "name": flag.name,
410
+ "status": flag.status,
411
+ "type": flag.flag_type,
412
+ "value": flag.value,
413
+ "rollout_percentage": flag.rollout_percentage,
414
+ "tags": flag.tags,
415
+ }
416
+
417
+ for tag in flag.tags:
418
+ if tag not in report["tags"]:
419
+ report["tags"][tag] = 0
420
+ report["tags"][tag] += 1
421
+
422
+ return report
423
+
424
+
425
+ # Global instance
426
+ feature_flags = FeatureFlagManager()
427
+
428
+
429
+ def is_enabled(
430
+ key: str, user_id: Optional[str] = None, context: Optional[Dict[str, Any]] = None
431
+ ) -> bool:
432
+ """Convenience function to check if feature flag is enabled"""
433
+ return feature_flags.is_enabled(key, user_id, context)