foundry-mcp 0.7.0__py3-none-any.whl → 0.8.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foundry_mcp/cli/__init__.py +0 -13
- foundry_mcp/cli/commands/session.py +1 -8
- foundry_mcp/cli/context.py +39 -0
- foundry_mcp/config.py +381 -7
- foundry_mcp/core/batch_operations.py +1196 -0
- foundry_mcp/core/discovery.py +1 -1
- foundry_mcp/core/llm_config.py +8 -0
- foundry_mcp/core/naming.py +25 -2
- foundry_mcp/core/prometheus.py +0 -13
- foundry_mcp/core/providers/__init__.py +12 -0
- foundry_mcp/core/providers/base.py +39 -0
- foundry_mcp/core/providers/claude.py +45 -1
- foundry_mcp/core/providers/codex.py +64 -3
- foundry_mcp/core/providers/cursor_agent.py +22 -3
- foundry_mcp/core/providers/detectors.py +34 -7
- foundry_mcp/core/providers/gemini.py +63 -1
- foundry_mcp/core/providers/opencode.py +95 -71
- foundry_mcp/core/providers/package-lock.json +4 -4
- foundry_mcp/core/providers/package.json +1 -1
- foundry_mcp/core/providers/validation.py +128 -0
- foundry_mcp/core/research/memory.py +103 -0
- foundry_mcp/core/research/models.py +783 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +5 -2
- foundry_mcp/core/research/workflows/base.py +106 -12
- foundry_mcp/core/research/workflows/consensus.py +160 -17
- foundry_mcp/core/research/workflows/deep_research.py +4020 -0
- foundry_mcp/core/responses.py +240 -0
- foundry_mcp/core/spec.py +1 -0
- foundry_mcp/core/task.py +141 -12
- foundry_mcp/core/validation.py +6 -1
- foundry_mcp/server.py +0 -52
- foundry_mcp/tools/unified/__init__.py +37 -18
- foundry_mcp/tools/unified/authoring.py +0 -33
- foundry_mcp/tools/unified/environment.py +202 -29
- foundry_mcp/tools/unified/plan.py +20 -1
- foundry_mcp/tools/unified/provider.py +0 -40
- foundry_mcp/tools/unified/research.py +644 -19
- foundry_mcp/tools/unified/review.py +5 -2
- foundry_mcp/tools/unified/review_helpers.py +16 -1
- foundry_mcp/tools/unified/server.py +9 -24
- foundry_mcp/tools/unified/task.py +528 -9
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/METADATA +2 -1
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/RECORD +52 -46
- foundry_mcp/cli/flags.py +0 -266
- foundry_mcp/core/feature_flags.py +0 -592
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,592 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Feature flags infrastructure for foundry-mcp.
|
|
3
|
-
|
|
4
|
-
Provides feature flag management with lifecycle states, percentage rollouts,
|
|
5
|
-
client-specific overrides, and testing utilities.
|
|
6
|
-
|
|
7
|
-
See docs/mcp_best_practices/14-feature-flags.md for guidance.
|
|
8
|
-
|
|
9
|
-
Example:
|
|
10
|
-
from foundry_mcp.core.feature_flags import (
|
|
11
|
-
FlagState, FeatureFlag, FeatureFlagRegistry, feature_flag, flag_override
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
# Define flags
|
|
15
|
-
registry = FeatureFlagRegistry()
|
|
16
|
-
registry.register(FeatureFlag(
|
|
17
|
-
name="new_algorithm",
|
|
18
|
-
description="Use improved processing algorithm",
|
|
19
|
-
state=FlagState.BETA,
|
|
20
|
-
default_enabled=False,
|
|
21
|
-
))
|
|
22
|
-
|
|
23
|
-
# Gate feature with decorator
|
|
24
|
-
@feature_flag("new_algorithm")
|
|
25
|
-
def process_data(data: dict) -> dict:
|
|
26
|
-
return improved_process(data)
|
|
27
|
-
|
|
28
|
-
# Test with override
|
|
29
|
-
with flag_override("new_algorithm", True):
|
|
30
|
-
result = process_data({"input": "test"})
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
import hashlib
|
|
34
|
-
import logging
|
|
35
|
-
from contextlib import contextmanager
|
|
36
|
-
from contextvars import ContextVar
|
|
37
|
-
from dataclasses import asdict, dataclass, field
|
|
38
|
-
from datetime import datetime, timezone
|
|
39
|
-
from enum import Enum
|
|
40
|
-
from functools import wraps
|
|
41
|
-
from typing import (
|
|
42
|
-
Any,
|
|
43
|
-
Callable,
|
|
44
|
-
Dict,
|
|
45
|
-
Generator,
|
|
46
|
-
Optional,
|
|
47
|
-
ParamSpec,
|
|
48
|
-
Set,
|
|
49
|
-
TypeVar,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
from foundry_mcp.core.responses import error_response
|
|
53
|
-
|
|
54
|
-
logger = logging.getLogger(__name__)
|
|
55
|
-
|
|
56
|
-
# Context variable for current client ID
|
|
57
|
-
current_client_id: ContextVar[str] = ContextVar("client_id", default="anonymous")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class FlagState(str, Enum):
|
|
61
|
-
"""Lifecycle state for feature flags.
|
|
62
|
-
|
|
63
|
-
Feature flags progress through states as they mature:
|
|
64
|
-
|
|
65
|
-
EXPERIMENTAL: Early development, opt-in only, may change without notice, no SLA
|
|
66
|
-
BETA: Feature-complete but not fully validated, default off, opt-in available, some SLA
|
|
67
|
-
STABLE: Production-ready, default on, full SLA guarantees
|
|
68
|
-
DEPRECATED: Being phased out, warns on use, will be removed after expiration
|
|
69
|
-
|
|
70
|
-
Example:
|
|
71
|
-
>>> flag = FeatureFlag(name="my_feature", state=FlagState.EXPERIMENTAL)
|
|
72
|
-
>>> if flag.state == FlagState.DEPRECATED:
|
|
73
|
-
... logger.warning("Feature is deprecated")
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
EXPERIMENTAL = "experimental"
|
|
77
|
-
BETA = "beta"
|
|
78
|
-
STABLE = "stable"
|
|
79
|
-
DEPRECATED = "deprecated"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@dataclass
|
|
83
|
-
class FeatureFlag:
|
|
84
|
-
"""Definition of a feature flag with lifecycle and rollout configuration.
|
|
85
|
-
|
|
86
|
-
Attributes:
|
|
87
|
-
name: Unique identifier for the flag (used in code and config)
|
|
88
|
-
description: Human-readable description of what this flag controls
|
|
89
|
-
state: Current lifecycle state (experimental/beta/stable/deprecated)
|
|
90
|
-
default_enabled: Whether the flag is enabled by default
|
|
91
|
-
created_at: When the flag was created (defaults to now)
|
|
92
|
-
expires_at: Optional expiration date after which flag should be removed
|
|
93
|
-
owner: Team or individual responsible for this flag
|
|
94
|
-
percentage_rollout: Percentage of clients that should have flag enabled (0-100)
|
|
95
|
-
allowed_clients: If non-empty, only these clients can access the flag
|
|
96
|
-
blocked_clients: These clients are explicitly denied access
|
|
97
|
-
dependencies: Other flags that must be enabled for this flag to be enabled
|
|
98
|
-
metadata: Additional key-value metadata for the flag
|
|
99
|
-
|
|
100
|
-
Example:
|
|
101
|
-
>>> flag = FeatureFlag(
|
|
102
|
-
... name="new_search",
|
|
103
|
-
... description="Enable new search algorithm",
|
|
104
|
-
... state=FlagState.BETA,
|
|
105
|
-
... default_enabled=False,
|
|
106
|
-
... percentage_rollout=25.0,
|
|
107
|
-
... owner="search-team",
|
|
108
|
-
... )
|
|
109
|
-
>>> flag.is_expired()
|
|
110
|
-
False
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
name: str
|
|
114
|
-
description: str
|
|
115
|
-
state: FlagState
|
|
116
|
-
default_enabled: bool
|
|
117
|
-
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
118
|
-
expires_at: Optional[datetime] = None
|
|
119
|
-
owner: str = ""
|
|
120
|
-
percentage_rollout: float = 100.0
|
|
121
|
-
allowed_clients: Set[str] = field(default_factory=set)
|
|
122
|
-
blocked_clients: Set[str] = field(default_factory=set)
|
|
123
|
-
dependencies: Set[str] = field(default_factory=set)
|
|
124
|
-
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
125
|
-
|
|
126
|
-
def is_expired(self) -> bool:
|
|
127
|
-
"""Check if the flag has passed its expiration date."""
|
|
128
|
-
if self.expires_at is None:
|
|
129
|
-
return False
|
|
130
|
-
return datetime.now(timezone.utc) > self.expires_at
|
|
131
|
-
|
|
132
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
133
|
-
"""Convert to dictionary for JSON serialization."""
|
|
134
|
-
return {
|
|
135
|
-
"name": self.name,
|
|
136
|
-
"description": self.description,
|
|
137
|
-
"state": self.state.value,
|
|
138
|
-
"default_enabled": self.default_enabled,
|
|
139
|
-
"created_at": self.created_at.isoformat(),
|
|
140
|
-
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
141
|
-
"owner": self.owner,
|
|
142
|
-
"percentage_rollout": self.percentage_rollout,
|
|
143
|
-
"allowed_clients": list(self.allowed_clients),
|
|
144
|
-
"blocked_clients": list(self.blocked_clients),
|
|
145
|
-
"dependencies": list(self.dependencies),
|
|
146
|
-
"metadata": self.metadata,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
class FeatureFlagRegistry:
|
|
151
|
-
"""Registry for managing feature flags with evaluation logic.
|
|
152
|
-
|
|
153
|
-
Provides flag registration, client-specific evaluation, percentage rollouts,
|
|
154
|
-
and override support for testing.
|
|
155
|
-
|
|
156
|
-
Example:
|
|
157
|
-
>>> registry = FeatureFlagRegistry()
|
|
158
|
-
>>> registry.register(FeatureFlag(
|
|
159
|
-
... name="new_feature",
|
|
160
|
-
... description="Test feature",
|
|
161
|
-
... state=FlagState.BETA,
|
|
162
|
-
... default_enabled=False,
|
|
163
|
-
... ))
|
|
164
|
-
>>> registry.is_enabled("new_feature", client_id="user123")
|
|
165
|
-
False
|
|
166
|
-
"""
|
|
167
|
-
|
|
168
|
-
def __init__(self) -> None:
|
|
169
|
-
"""Initialize an empty flag registry."""
|
|
170
|
-
self._flags: Dict[str, FeatureFlag] = {}
|
|
171
|
-
self._overrides: Dict[
|
|
172
|
-
str, Dict[str, bool]
|
|
173
|
-
] = {} # client_id -> flag_name -> value
|
|
174
|
-
|
|
175
|
-
def register(self, flag: FeatureFlag) -> None:
|
|
176
|
-
"""Register a feature flag.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
flag: The feature flag to register
|
|
180
|
-
|
|
181
|
-
Raises:
|
|
182
|
-
ValueError: If a flag with the same name already exists
|
|
183
|
-
"""
|
|
184
|
-
if flag.name in self._flags:
|
|
185
|
-
raise ValueError(f"Flag '{flag.name}' is already registered")
|
|
186
|
-
self._flags[flag.name] = flag
|
|
187
|
-
logger.debug(f"Registered feature flag: {flag.name} ({flag.state.value})")
|
|
188
|
-
|
|
189
|
-
def get(self, flag_name: str) -> Optional[FeatureFlag]:
|
|
190
|
-
"""Get a flag by name.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
flag_name: Name of the flag to retrieve
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
The FeatureFlag if found, None otherwise
|
|
197
|
-
"""
|
|
198
|
-
return self._flags.get(flag_name)
|
|
199
|
-
|
|
200
|
-
def is_enabled(
|
|
201
|
-
self,
|
|
202
|
-
flag_name: str,
|
|
203
|
-
client_id: Optional[str] = None,
|
|
204
|
-
default: bool = False,
|
|
205
|
-
) -> bool:
|
|
206
|
-
"""Check if a feature flag is enabled for a client.
|
|
207
|
-
|
|
208
|
-
Evaluation order:
|
|
209
|
-
1. Check for client-specific override
|
|
210
|
-
2. Check if flag exists
|
|
211
|
-
3. Check if flag is expired
|
|
212
|
-
4. Check client blocklist
|
|
213
|
-
5. Check client allowlist
|
|
214
|
-
6. Check flag dependencies
|
|
215
|
-
7. Evaluate percentage rollout
|
|
216
|
-
8. Return default_enabled value
|
|
217
|
-
|
|
218
|
-
Args:
|
|
219
|
-
flag_name: Name of the flag to check
|
|
220
|
-
client_id: Client ID for evaluation (defaults to context variable)
|
|
221
|
-
default: Value to return if flag doesn't exist
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
True if the flag is enabled for this client, False otherwise
|
|
225
|
-
"""
|
|
226
|
-
client_id = client_id or current_client_id.get()
|
|
227
|
-
|
|
228
|
-
# Check for client-specific override first
|
|
229
|
-
if client_id in self._overrides:
|
|
230
|
-
if flag_name in self._overrides[client_id]:
|
|
231
|
-
return self._overrides[client_id][flag_name]
|
|
232
|
-
|
|
233
|
-
# Check if flag exists
|
|
234
|
-
flag = self._flags.get(flag_name)
|
|
235
|
-
if not flag:
|
|
236
|
-
logger.warning(f"Unknown feature flag: {flag_name}")
|
|
237
|
-
return default
|
|
238
|
-
|
|
239
|
-
# Warn if deprecated
|
|
240
|
-
if flag.state == FlagState.DEPRECATED:
|
|
241
|
-
logger.warning(
|
|
242
|
-
f"Deprecated feature flag '{flag_name}' accessed",
|
|
243
|
-
extra={"client_id": client_id, "flag": flag_name},
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
# Check expiration
|
|
247
|
-
if flag.is_expired():
|
|
248
|
-
logger.warning(f"Expired feature flag: {flag_name}")
|
|
249
|
-
return default
|
|
250
|
-
|
|
251
|
-
# Check blocklist
|
|
252
|
-
if flag.blocked_clients and client_id in flag.blocked_clients:
|
|
253
|
-
return False
|
|
254
|
-
|
|
255
|
-
# Check allowlist (empty means all allowed)
|
|
256
|
-
if flag.allowed_clients and client_id not in flag.allowed_clients:
|
|
257
|
-
return False
|
|
258
|
-
|
|
259
|
-
# Check dependencies
|
|
260
|
-
for dep_flag in flag.dependencies:
|
|
261
|
-
if not self.is_enabled(dep_flag, client_id):
|
|
262
|
-
return False
|
|
263
|
-
|
|
264
|
-
# Evaluate percentage rollout
|
|
265
|
-
if not self._evaluate_percentage(flag, client_id):
|
|
266
|
-
return False
|
|
267
|
-
|
|
268
|
-
return flag.default_enabled
|
|
269
|
-
|
|
270
|
-
def _evaluate_percentage(self, flag: FeatureFlag, client_id: str) -> bool:
|
|
271
|
-
"""Evaluate percentage-based rollout using deterministic hashing.
|
|
272
|
-
|
|
273
|
-
Args:
|
|
274
|
-
flag: The feature flag to evaluate
|
|
275
|
-
client_id: Client ID for bucket assignment
|
|
276
|
-
|
|
277
|
-
Returns:
|
|
278
|
-
True if client falls within rollout percentage, False otherwise
|
|
279
|
-
"""
|
|
280
|
-
if flag.percentage_rollout >= 100.0:
|
|
281
|
-
return True
|
|
282
|
-
|
|
283
|
-
if flag.percentage_rollout <= 0.0:
|
|
284
|
-
return False
|
|
285
|
-
|
|
286
|
-
# Use consistent hashing for stable bucket assignment
|
|
287
|
-
hash_input = f"{flag.name}:{client_id}"
|
|
288
|
-
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
|
|
289
|
-
bucket = (hash_value % 100) + 1
|
|
290
|
-
|
|
291
|
-
return bucket <= flag.percentage_rollout
|
|
292
|
-
|
|
293
|
-
def set_override(self, client_id: str, flag_name: str, enabled: bool) -> None:
|
|
294
|
-
"""Set a client-specific override for a flag.
|
|
295
|
-
|
|
296
|
-
Args:
|
|
297
|
-
client_id: The client to override for
|
|
298
|
-
flag_name: Name of the flag to override
|
|
299
|
-
enabled: Override value
|
|
300
|
-
"""
|
|
301
|
-
if client_id not in self._overrides:
|
|
302
|
-
self._overrides[client_id] = {}
|
|
303
|
-
self._overrides[client_id][flag_name] = enabled
|
|
304
|
-
logger.debug(f"Set override: {flag_name}={enabled} for client {client_id}")
|
|
305
|
-
|
|
306
|
-
def clear_override(self, client_id: str, flag_name: str) -> None:
|
|
307
|
-
"""Clear a client-specific override.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
client_id: The client to clear override for
|
|
311
|
-
flag_name: Name of the flag to clear
|
|
312
|
-
"""
|
|
313
|
-
if client_id in self._overrides:
|
|
314
|
-
self._overrides[client_id].pop(flag_name, None)
|
|
315
|
-
if not self._overrides[client_id]:
|
|
316
|
-
del self._overrides[client_id]
|
|
317
|
-
|
|
318
|
-
def clear_all_overrides(self, client_id: Optional[str] = None) -> None:
|
|
319
|
-
"""Clear all overrides, optionally for a specific client.
|
|
320
|
-
|
|
321
|
-
Args:
|
|
322
|
-
client_id: If provided, only clear overrides for this client
|
|
323
|
-
"""
|
|
324
|
-
if client_id:
|
|
325
|
-
self._overrides.pop(client_id, None)
|
|
326
|
-
else:
|
|
327
|
-
self._overrides.clear()
|
|
328
|
-
|
|
329
|
-
def get_flags_for_capabilities(
|
|
330
|
-
self,
|
|
331
|
-
client_id: Optional[str] = None,
|
|
332
|
-
) -> Dict[str, Dict[str, Any]]:
|
|
333
|
-
"""Get flag status for capabilities endpoint.
|
|
334
|
-
|
|
335
|
-
Returns a dictionary suitable for including in a capabilities response,
|
|
336
|
-
showing each flag's enabled status, state, and description.
|
|
337
|
-
|
|
338
|
-
Args:
|
|
339
|
-
client_id: Client ID for evaluation (defaults to context variable)
|
|
340
|
-
|
|
341
|
-
Returns:
|
|
342
|
-
Dictionary mapping flag names to their status information
|
|
343
|
-
"""
|
|
344
|
-
client_id = client_id or current_client_id.get()
|
|
345
|
-
result = {}
|
|
346
|
-
|
|
347
|
-
for name, flag in self._flags.items():
|
|
348
|
-
status = {
|
|
349
|
-
"enabled": self.is_enabled(name, client_id),
|
|
350
|
-
"state": flag.state.value,
|
|
351
|
-
"description": flag.description,
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if flag.state == FlagState.DEPRECATED:
|
|
355
|
-
expires_str = (
|
|
356
|
-
flag.expires_at.isoformat() if flag.expires_at else "unspecified"
|
|
357
|
-
)
|
|
358
|
-
status["deprecation_notice"] = (
|
|
359
|
-
f"This feature is deprecated and will be removed after {expires_str}"
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
result[name] = status
|
|
363
|
-
|
|
364
|
-
return result
|
|
365
|
-
|
|
366
|
-
def list_flags(self) -> Dict[str, FeatureFlag]:
|
|
367
|
-
"""Get all registered flags.
|
|
368
|
-
|
|
369
|
-
Returns:
|
|
370
|
-
Dictionary mapping flag names to FeatureFlag objects
|
|
371
|
-
"""
|
|
372
|
-
return dict(self._flags)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# Global registry instance
|
|
376
|
-
_default_registry = FeatureFlagRegistry()
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def _register_builtin_flags(registry: FeatureFlagRegistry) -> None:
|
|
380
|
-
"""Register Foundry MCP built-in flags.
|
|
381
|
-
|
|
382
|
-
This keeps commonly-referenced flags discoverable and avoids noisy
|
|
383
|
-
"Unknown feature flag" warnings when running without env overrides.
|
|
384
|
-
"""
|
|
385
|
-
|
|
386
|
-
builtin_flags = [
|
|
387
|
-
FeatureFlag(
|
|
388
|
-
name="unified_tools_phase2",
|
|
389
|
-
description="Enable unified routers for medium tool families",
|
|
390
|
-
state=FlagState.BETA,
|
|
391
|
-
default_enabled=False,
|
|
392
|
-
percentage_rollout=0.0,
|
|
393
|
-
owner="foundry-mcp core",
|
|
394
|
-
),
|
|
395
|
-
FeatureFlag(
|
|
396
|
-
name="unified_tools_phase3",
|
|
397
|
-
description="Enable unified routers for large tool families",
|
|
398
|
-
state=FlagState.BETA,
|
|
399
|
-
default_enabled=False,
|
|
400
|
-
percentage_rollout=0.0,
|
|
401
|
-
owner="foundry-mcp core",
|
|
402
|
-
),
|
|
403
|
-
FeatureFlag(
|
|
404
|
-
name="unified_manifest",
|
|
405
|
-
description="Expose only the 17 unified tools for discovery/token reduction",
|
|
406
|
-
state=FlagState.BETA,
|
|
407
|
-
default_enabled=False,
|
|
408
|
-
percentage_rollout=0.0,
|
|
409
|
-
owner="foundry-mcp core",
|
|
410
|
-
),
|
|
411
|
-
]
|
|
412
|
-
|
|
413
|
-
for flag in builtin_flags:
|
|
414
|
-
if registry.get(flag.name) is not None:
|
|
415
|
-
continue
|
|
416
|
-
try:
|
|
417
|
-
registry.register(flag)
|
|
418
|
-
except ValueError:
|
|
419
|
-
# Already registered (race or re-import)
|
|
420
|
-
continue
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
_register_builtin_flags(_default_registry)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
def get_registry() -> FeatureFlagRegistry:
|
|
427
|
-
"""Get the default feature flag registry."""
|
|
428
|
-
return _default_registry
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
# Alias for server.py compatibility
|
|
432
|
-
get_flag_service = get_registry
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
P = ParamSpec("P")
|
|
436
|
-
R = TypeVar("R")
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def feature_flag(
|
|
440
|
-
flag_name: str,
|
|
441
|
-
*,
|
|
442
|
-
fallback: Optional[Callable[P, Any]] = None,
|
|
443
|
-
registry: Optional[FeatureFlagRegistry] = None,
|
|
444
|
-
) -> Callable[[Callable[P, R]], Callable[P, Any]]:
|
|
445
|
-
"""Decorator to gate function execution behind a feature flag.
|
|
446
|
-
|
|
447
|
-
When the feature flag is not enabled for the current client, the decorated
|
|
448
|
-
function will not execute. Instead, it returns a FEATURE_DISABLED error
|
|
449
|
-
response (or invokes the fallback if provided).
|
|
450
|
-
|
|
451
|
-
Args:
|
|
452
|
-
flag_name: Name of the feature flag to check
|
|
453
|
-
fallback: Optional fallback function to call when flag is disabled.
|
|
454
|
-
If provided, this function is called with the same arguments.
|
|
455
|
-
If not provided, returns a FEATURE_DISABLED error response.
|
|
456
|
-
registry: Optional registry to use. Defaults to the global registry.
|
|
457
|
-
|
|
458
|
-
Returns:
|
|
459
|
-
Decorated function that checks the flag before execution
|
|
460
|
-
|
|
461
|
-
Example:
|
|
462
|
-
>>> @feature_flag("new_algorithm")
|
|
463
|
-
... def process_data(data: dict) -> dict:
|
|
464
|
-
... return {"processed": True}
|
|
465
|
-
...
|
|
466
|
-
>>> # When flag is disabled, returns error response
|
|
467
|
-
>>> result = process_data({"input": "test"})
|
|
468
|
-
>>> result["success"]
|
|
469
|
-
False
|
|
470
|
-
>>> result["data"]["error_code"]
|
|
471
|
-
'FEATURE_DISABLED'
|
|
472
|
-
|
|
473
|
-
>>> # With fallback function
|
|
474
|
-
>>> @feature_flag("new_algorithm", fallback=legacy_process)
|
|
475
|
-
... def process_data(data: dict) -> dict:
|
|
476
|
-
... return {"processed": True, "algorithm": "new"}
|
|
477
|
-
...
|
|
478
|
-
>>> # When flag is disabled, calls legacy_process instead
|
|
479
|
-
"""
|
|
480
|
-
|
|
481
|
-
def decorator(func: Callable[P, R]) -> Callable[P, Any]:
|
|
482
|
-
@wraps(func)
|
|
483
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
|
|
484
|
-
reg = registry or get_registry()
|
|
485
|
-
|
|
486
|
-
if not reg.is_enabled(flag_name):
|
|
487
|
-
logger.debug(
|
|
488
|
-
f"Feature flag '{flag_name}' is disabled, "
|
|
489
|
-
f"blocking execution of {func.__name__}"
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
if fallback is not None:
|
|
493
|
-
return fallback(*args, **kwargs)
|
|
494
|
-
|
|
495
|
-
# Return FEATURE_DISABLED error response
|
|
496
|
-
response = error_response(
|
|
497
|
-
message=f"Feature '{flag_name}' is not enabled",
|
|
498
|
-
error_code="FEATURE_DISABLED",
|
|
499
|
-
error_type="feature_flag",
|
|
500
|
-
data={
|
|
501
|
-
"feature": flag_name,
|
|
502
|
-
},
|
|
503
|
-
remediation="Contact support to enable this feature or check feature flag configuration",
|
|
504
|
-
)
|
|
505
|
-
return asdict(response)
|
|
506
|
-
|
|
507
|
-
return func(*args, **kwargs)
|
|
508
|
-
|
|
509
|
-
return wrapper
|
|
510
|
-
|
|
511
|
-
return decorator
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
@contextmanager
|
|
515
|
-
def flag_override(
|
|
516
|
-
flag_name: str,
|
|
517
|
-
enabled: bool,
|
|
518
|
-
*,
|
|
519
|
-
client_id: Optional[str] = None,
|
|
520
|
-
registry: Optional[FeatureFlagRegistry] = None,
|
|
521
|
-
) -> Generator[None, None, None]:
|
|
522
|
-
"""Context manager for temporarily overriding a feature flag value.
|
|
523
|
-
|
|
524
|
-
Useful for testing code paths that depend on feature flags without
|
|
525
|
-
modifying the actual flag configuration. The override is automatically
|
|
526
|
-
cleaned up when the context exits, even if an exception occurs.
|
|
527
|
-
|
|
528
|
-
Args:
|
|
529
|
-
flag_name: Name of the feature flag to override
|
|
530
|
-
enabled: The override value (True to enable, False to disable)
|
|
531
|
-
client_id: Optional client ID for the override (defaults to current_client_id)
|
|
532
|
-
registry: Optional registry to use (defaults to global registry)
|
|
533
|
-
|
|
534
|
-
Yields:
|
|
535
|
-
None
|
|
536
|
-
|
|
537
|
-
Example:
|
|
538
|
-
>>> registry = FeatureFlagRegistry()
|
|
539
|
-
>>> registry.register(FeatureFlag(
|
|
540
|
-
... name="new_feature",
|
|
541
|
-
... description="Test",
|
|
542
|
-
... state=FlagState.BETA,
|
|
543
|
-
... default_enabled=False,
|
|
544
|
-
... ))
|
|
545
|
-
>>> # Flag is disabled by default
|
|
546
|
-
>>> registry.is_enabled("new_feature")
|
|
547
|
-
False
|
|
548
|
-
>>> # Override to enable temporarily
|
|
549
|
-
>>> with flag_override("new_feature", True, registry=registry):
|
|
550
|
-
... registry.is_enabled("new_feature")
|
|
551
|
-
True
|
|
552
|
-
>>> # Back to original state after context
|
|
553
|
-
>>> registry.is_enabled("new_feature")
|
|
554
|
-
False
|
|
555
|
-
|
|
556
|
-
>>> # Use in tests to force flag states
|
|
557
|
-
>>> with flag_override("new_feature", True):
|
|
558
|
-
... result = my_flagged_function() # Runs with flag enabled
|
|
559
|
-
"""
|
|
560
|
-
reg = registry or get_registry()
|
|
561
|
-
cid = client_id or current_client_id.get()
|
|
562
|
-
|
|
563
|
-
# Check if there's an existing override we need to restore
|
|
564
|
-
had_previous_override = False
|
|
565
|
-
previous_override_value = False
|
|
566
|
-
|
|
567
|
-
if cid in reg._overrides and flag_name in reg._overrides[cid]:
|
|
568
|
-
had_previous_override = True
|
|
569
|
-
previous_override_value = reg._overrides[cid][flag_name]
|
|
570
|
-
|
|
571
|
-
try:
|
|
572
|
-
reg.set_override(cid, flag_name, enabled)
|
|
573
|
-
yield
|
|
574
|
-
finally:
|
|
575
|
-
# Restore previous state
|
|
576
|
-
if had_previous_override:
|
|
577
|
-
reg.set_override(cid, flag_name, previous_override_value)
|
|
578
|
-
else:
|
|
579
|
-
reg.clear_override(cid, flag_name)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
# Export all public symbols
|
|
583
|
-
__all__ = [
|
|
584
|
-
"FlagState",
|
|
585
|
-
"FeatureFlag",
|
|
586
|
-
"FeatureFlagRegistry",
|
|
587
|
-
"current_client_id",
|
|
588
|
-
"get_registry",
|
|
589
|
-
"get_flag_service",
|
|
590
|
-
"feature_flag",
|
|
591
|
-
"flag_override",
|
|
592
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|