flagura-sdk 1.0.0__tar.gz

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.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: flagura-sdk
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK and OpenFeature Provider for Flagura
5
+ Author-email: Dhawal Dyavanpalli <dhawalhost@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dhawalhost/flagura
8
+ Project-URL: Repository, https://github.com/dhawalhost/flagura
9
+ Keywords: feature-flags,feature-toggles,openfeature,flagura,a-b-testing
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: requests>=2.28.0
13
+ Requires-Dist: openfeature-sdk>=0.7.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
16
+ Requires-Dist: build>=1.0.0; extra == "dev"
17
+ Requires-Dist: twine>=4.0.0; extra == "dev"
18
+
19
+ # ⚡ Flagura Python SDK
20
+
21
+ Official Python client for the **Flagura Feature Flag Platform**, supporting:
22
+ - High-performance evaluations
23
+ - **Real-Time SSE Flag Streaming (`<5ms` sync)**
24
+ - **CNCF OpenFeature Provider**
25
+ - FastAPI, Django, Flask, Celery, and AI Agent compatibility
26
+
27
+ ---
28
+
29
+ ## 📦 Installation
30
+
31
+ ```bash
32
+ pip install flagura
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🚀 Quickstart
38
+
39
+ ### 1. Direct Client with Real-Time SSE Streaming
40
+
41
+ ```python
42
+ from flagura import FlaguraClient, EvaluationContext
43
+
44
+ # Initialize client with real-time SSE streaming
45
+ client = FlaguraClient(
46
+ endpoint="https://flagura.dhawalhost.com",
47
+ api_key="your-api-key",
48
+ project_id="proj_default", # optional: project scoping
49
+ default_environment="production",
50
+ enable_streaming=True, # <5ms live flag updates
51
+ )
52
+
53
+ # Register real-time change listener
54
+ client.on_update(lambda flags: print(f"Flags updated in real-time! Count: {len(flags)}"))
55
+
56
+ # Evaluate flag
57
+ context = EvaluationContext(
58
+ user_id="usr_dhawal_01",
59
+ email="dhawal@flagura.dev",
60
+ tier="enterprise",
61
+ )
62
+
63
+ if client.is_enabled("ai-smart-search", context):
64
+ variant = client.get_variant("ai-smart-search", context)
65
+ print(f"AI Smart Search is ON! Variant: {variant}")
66
+
67
+ # Cleanup on shutdown
68
+ client.close()
69
+ ```
70
+
71
+ ---
72
+
73
+ ### 2. OpenFeature Universal Provider
74
+
75
+ ```python
76
+ from openfeature import api
77
+ from openfeature.evaluation_context import EvaluationContext
78
+ from flagura.openfeature_provider import FlaguraOpenFeatureProvider
79
+
80
+ # Register Flagura as OpenFeature provider
81
+ api.set_provider(FlaguraOpenFeatureProvider(
82
+ endpoint="https://flagura.dhawalhost.com",
83
+ api_key="your-api-key",
84
+ enable_streaming=True,
85
+ ))
86
+ of_client = api.get_client()
87
+
88
+ # Evaluate with OpenFeature standard APIs
89
+ ctx = EvaluationContext(targeting_key="usr_dhawal_01", attributes={"email": "dhawal@flagura.dev"})
90
+ is_enabled = of_client.get_boolean_value("ai-smart-search", False, ctx)
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 📄 License
96
+ MIT
@@ -0,0 +1,78 @@
1
+ # ⚡ Flagura Python SDK
2
+
3
+ Official Python client for the **Flagura Feature Flag Platform**, supporting:
4
+ - High-performance evaluations
5
+ - **Real-Time SSE Flag Streaming (`<5ms` sync)**
6
+ - **CNCF OpenFeature Provider**
7
+ - FastAPI, Django, Flask, Celery, and AI Agent compatibility
8
+
9
+ ---
10
+
11
+ ## 📦 Installation
12
+
13
+ ```bash
14
+ pip install flagura
15
+ ```
16
+
17
+ ---
18
+
19
+ ## 🚀 Quickstart
20
+
21
+ ### 1. Direct Client with Real-Time SSE Streaming
22
+
23
+ ```python
24
+ from flagura import FlaguraClient, EvaluationContext
25
+
26
+ # Initialize client with real-time SSE streaming
27
+ client = FlaguraClient(
28
+ endpoint="https://flagura.dhawalhost.com",
29
+ api_key="your-api-key",
30
+ project_id="proj_default", # optional: project scoping
31
+ default_environment="production",
32
+ enable_streaming=True, # <5ms live flag updates
33
+ )
34
+
35
+ # Register real-time change listener
36
+ client.on_update(lambda flags: print(f"Flags updated in real-time! Count: {len(flags)}"))
37
+
38
+ # Evaluate flag
39
+ context = EvaluationContext(
40
+ user_id="usr_dhawal_01",
41
+ email="dhawal@flagura.dev",
42
+ tier="enterprise",
43
+ )
44
+
45
+ if client.is_enabled("ai-smart-search", context):
46
+ variant = client.get_variant("ai-smart-search", context)
47
+ print(f"AI Smart Search is ON! Variant: {variant}")
48
+
49
+ # Cleanup on shutdown
50
+ client.close()
51
+ ```
52
+
53
+ ---
54
+
55
+ ### 2. OpenFeature Universal Provider
56
+
57
+ ```python
58
+ from openfeature import api
59
+ from openfeature.evaluation_context import EvaluationContext
60
+ from flagura.openfeature_provider import FlaguraOpenFeatureProvider
61
+
62
+ # Register Flagura as OpenFeature provider
63
+ api.set_provider(FlaguraOpenFeatureProvider(
64
+ endpoint="https://flagura.dhawalhost.com",
65
+ api_key="your-api-key",
66
+ enable_streaming=True,
67
+ ))
68
+ of_client = api.get_client()
69
+
70
+ # Evaluate with OpenFeature standard APIs
71
+ ctx = EvaluationContext(targeting_key="usr_dhawal_01", attributes={"email": "dhawal@flagura.dev"})
72
+ is_enabled = of_client.get_boolean_value("ai-smart-search", False, ctx)
73
+ ```
74
+
75
+ ---
76
+
77
+ ## 📄 License
78
+ MIT
@@ -0,0 +1,5 @@
1
+ from .client import FlaguraClient, EvaluationContext, EvaluationResult
2
+ from .openfeature_provider import FlaguraOpenFeatureProvider
3
+
4
+ __all__ = ["FlaguraClient", "EvaluationContext", "EvaluationResult", "FlaguraOpenFeatureProvider"]
5
+ __version__ = "1.1.0"
@@ -0,0 +1,234 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Callable, Dict, List, Optional
3
+ import urllib.request
4
+ import urllib.error
5
+ import json
6
+ import threading
7
+ import time
8
+ import datetime
9
+
10
+
11
+ @dataclass
12
+ class EvaluationContext:
13
+ user_id: str
14
+ email: Optional[str] = None
15
+ country: Optional[str] = None
16
+ role: Optional[str] = None
17
+ tier: Optional[str] = None
18
+ environment: str = "production"
19
+ custom: Dict[str, Any] = field(default_factory=dict)
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ d: Dict[str, Any] = {
23
+ "user_id": self.user_id,
24
+ "environment": self.environment,
25
+ }
26
+ if self.email:
27
+ d["email"] = self.email
28
+ if self.country:
29
+ d["country"] = self.country
30
+ if self.role:
31
+ d["role"] = self.role
32
+ if self.tier:
33
+ d["tier"] = self.tier
34
+ if self.custom:
35
+ d["custom"] = self.custom
36
+ return d
37
+
38
+
39
+ @dataclass
40
+ class EvaluationResult:
41
+ flag_key: str
42
+ enabled: bool
43
+ variant: str = "off"
44
+ value: Any = False
45
+ reason: str = ""
46
+ bucket: Optional[float] = None
47
+ latency_ns: int = 0
48
+ latency_us: float = 0.0
49
+
50
+
51
+ class FlaguraClient:
52
+ """Official Python Client for Flagura Feature Flag Platform."""
53
+
54
+ def __init__(
55
+ self,
56
+ endpoint: str = "http://localhost:3000",
57
+ api_key: Optional[str] = None,
58
+ project_id: Optional[str] = None,
59
+ default_environment: str = "production",
60
+ timeout: float = 5.0,
61
+ enable_streaming: bool = False,
62
+ ):
63
+ self.endpoint = endpoint.rstrip("/")
64
+ self.api_key = api_key
65
+ self.project_id = project_id
66
+ self.default_environment = default_environment
67
+ self.timeout = timeout
68
+ self._local_flags: Dict[str, Any] = {}
69
+ self._listeners: List[Callable[[Dict[str, Any]], None]] = []
70
+ self._stop_event = threading.Event()
71
+ self._stream_thread: Optional[threading.Thread] = None
72
+
73
+ if enable_streaming:
74
+ self._start_streaming()
75
+
76
+ def on_update(self, callback: Callable[[Dict[str, Any]], None]) -> None:
77
+ """Register a callback invoked when feature flags are updated in real time."""
78
+ self._listeners.append(callback)
79
+
80
+ def _start_streaming(self) -> None:
81
+ self._stream_thread = threading.Thread(target=self._stream_worker, daemon=True)
82
+ self._stream_thread.start()
83
+
84
+ def _stream_worker(self) -> None:
85
+ url = f"{self.endpoint}/api/v1/flags/stream"
86
+ headers = {"Accept": "text/event-stream"}
87
+ if self.api_key:
88
+ headers["Authorization"] = f"Bearer {self.api_key}"
89
+ if self.project_id:
90
+ headers["X-Project-ID"] = self.project_id
91
+
92
+ while not self._stop_event.is_set():
93
+ try:
94
+ req = urllib.request.Request(url, headers=headers)
95
+ with urllib.request.urlopen(req, timeout=30) as resp:
96
+ for line_bytes in resp:
97
+ if self._stop_event.is_set():
98
+ break
99
+ line = line_bytes.decode("utf-8").strip()
100
+ if line.startswith("data:"):
101
+ try:
102
+ data_str = line[5:].strip()
103
+ if data_str == "ping" or not data_str:
104
+ continue
105
+ payload = json.loads(data_str)
106
+ if isinstance(payload, list):
107
+ self._local_flags = {f["key"]: f for f in payload if "key" in f}
108
+ elif isinstance(payload, dict) and "flags" in payload:
109
+ raw_flags = payload["flags"]
110
+ if isinstance(raw_flags, list):
111
+ self._local_flags = {f["key"]: f for f in raw_flags if "key" in f}
112
+ elif isinstance(raw_flags, dict):
113
+ self._local_flags = raw_flags
114
+ for listener in self._listeners:
115
+ listener(dict(self._local_flags))
116
+ except Exception:
117
+ pass
118
+ except Exception:
119
+ if not self._stop_event.is_set():
120
+ time.sleep(3.0)
121
+
122
+ def is_enabled(self, flag_key: str, context: EvaluationContext) -> bool:
123
+ """Check if a boolean flag is enabled for the given context."""
124
+ try:
125
+ res = self.evaluate(flag_key, context)
126
+ return bool(res.enabled)
127
+ except Exception:
128
+ return False
129
+
130
+ def get_variant(self, flag_key: str, context: EvaluationContext, fallback: str = "off") -> str:
131
+ """Retrieve the assigned variant string for a multivariate flag."""
132
+ try:
133
+ res = self.evaluate(flag_key, context)
134
+ return res.variant if res.variant and res.variant != "off" else fallback
135
+ except Exception:
136
+ return fallback
137
+
138
+ def evaluate(self, flag_key: str, context: EvaluationContext) -> EvaluationResult:
139
+ """Evaluate a single feature flag."""
140
+ results = self.evaluate_batch([flag_key], context)
141
+ if flag_key in results:
142
+ return results[flag_key]
143
+ return EvaluationResult(
144
+ flag_key=flag_key,
145
+ enabled=False,
146
+ variant="off",
147
+ value=False,
148
+ reason="FLAG_NOT_FOUND",
149
+ )
150
+
151
+ def evaluate_batch(self, flag_keys: List[str], context: EvaluationContext) -> Dict[str, EvaluationResult]:
152
+ """Evaluate multiple feature flags concurrently in a single HTTP payload."""
153
+ ctx_dict = context.to_dict()
154
+ if "environment" not in ctx_dict or not ctx_dict["environment"]:
155
+ ctx_dict["environment"] = self.default_environment
156
+
157
+ payload = {
158
+ "flags": flag_keys,
159
+ "context": ctx_dict,
160
+ }
161
+
162
+ req_data = json.dumps(payload).encode("utf-8")
163
+ headers = {"Content-Type": "application/json"}
164
+ if self.api_key:
165
+ headers["Authorization"] = f"Bearer {self.api_key}"
166
+ if self.project_id:
167
+ headers["X-Project-ID"] = self.project_id
168
+
169
+ req = urllib.request.Request(
170
+ f"{self.endpoint}/api/v1/evaluate",
171
+ data=req_data,
172
+ headers=headers,
173
+ method="POST",
174
+ )
175
+
176
+ try:
177
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
178
+ data = json.loads(resp.read().decode("utf-8"))
179
+ results: Dict[str, EvaluationResult] = {}
180
+ raw_results = data.get("results", {})
181
+
182
+ for k, v in raw_results.items():
183
+ results[k] = EvaluationResult(
184
+ flag_key=v.get("flag_key", k),
185
+ enabled=v.get("enabled", False),
186
+ variant=v.get("variant", "off"),
187
+ value=v.get("value", False),
188
+ reason=v.get("reason", ""),
189
+ bucket=v.get("bucket"),
190
+ latency_ns=v.get("latency_ns", 0),
191
+ latency_us=v.get("latency_us", 0.0),
192
+ )
193
+ return results
194
+ except Exception as e:
195
+ raise RuntimeError(f"Flagura evaluation request failed: {e}") from e
196
+
197
+ def track(self, flag_key: str, variant: str, metric_name: str, value: float = 1.0, user_id: str = "") -> None:
198
+ """Track an experiment conversion or numeric metric event."""
199
+ payload = {
200
+ "events": [
201
+ {
202
+ "flag_key": flag_key,
203
+ "project_id": self.project_id,
204
+ "variant": variant,
205
+ "metric_name": metric_name,
206
+ "value": value,
207
+ "user_id": user_id,
208
+ "environment": self.default_environment,
209
+ "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
210
+ }
211
+ ]
212
+ }
213
+ req_data = json.dumps(payload).encode("utf-8")
214
+ headers = {"Content-Type": "application/json"}
215
+ if self.api_key:
216
+ headers["Authorization"] = f"Bearer {self.api_key}"
217
+ if self.project_id:
218
+ headers["X-Project-ID"] = self.project_id
219
+
220
+ req = urllib.request.Request(
221
+ f"{self.endpoint}/api/v1/telemetry/events",
222
+ data=req_data,
223
+ headers=headers,
224
+ method="POST",
225
+ )
226
+ try:
227
+ with urllib.request.urlopen(req, timeout=self.timeout):
228
+ pass
229
+ except Exception:
230
+ pass
231
+
232
+ def close(self) -> None:
233
+ """Close active background stream threads."""
234
+ self._stop_event.set()
@@ -0,0 +1,266 @@
1
+ from typing import Any, Dict, Optional
2
+ from .client import FlaguraClient, EvaluationContext as FlaguraContext
3
+
4
+
5
+ class ResolutionDetails:
6
+ """Represents flag resolution details conforming to OpenFeature spec."""
7
+
8
+ def __init__(
9
+ self,
10
+ value: Any,
11
+ variant: Optional[str] = None,
12
+ reason: Optional[str] = None,
13
+ error_code: Optional[str] = None,
14
+ error_message: Optional[str] = None,
15
+ ):
16
+ self.value = value
17
+ self.variant = variant
18
+ self.reason = reason
19
+ self.error_code = error_code
20
+ self.error_message = error_message
21
+
22
+ def __repr__(self) -> str:
23
+ return f"ResolutionDetails(value={self.value!r}, variant={self.variant!r}, reason={self.reason!r})"
24
+
25
+
26
+ class FlaguraOpenFeatureProvider:
27
+ """Official Flagura OpenFeature Provider for Python applications."""
28
+
29
+ def __init__(self, client: Optional[FlaguraClient] = None, **kwargs: Any):
30
+ if client is not None:
31
+ self.client = client
32
+ else:
33
+ self.client = FlaguraClient(**kwargs)
34
+ self.name = "flagura-python-provider"
35
+
36
+ def get_metadata(self) -> Dict[str, str]:
37
+ return {"name": self.name}
38
+
39
+ def _map_context(self, context: Any) -> FlaguraContext:
40
+ if context is None:
41
+ return FlaguraContext(user_id="anonymous")
42
+
43
+ # OpenFeature evaluation context in Python can be an object with targeting_key and attributes dict
44
+ user_id = getattr(context, "targeting_key", None)
45
+ attributes = getattr(context, "attributes", {})
46
+
47
+ if isinstance(context, dict):
48
+ user_id = context.get("targeting_key") or context.get("user_id") or context.get("userId")
49
+ attributes = context
50
+
51
+ if not user_id:
52
+ user_id = attributes.get("user_id") or attributes.get("userId") or "anonymous"
53
+
54
+ return FlaguraContext(
55
+ user_id=str(user_id),
56
+ email=attributes.get("email"),
57
+ country=attributes.get("country"),
58
+ role=attributes.get("role"),
59
+ tier=attributes.get("tier"),
60
+ environment=attributes.get("environment", self.client.default_environment),
61
+ custom={
62
+ k: v
63
+ for k, v in attributes.items()
64
+ if k not in {"targeting_key", "user_id", "userId", "email", "country", "role", "tier", "environment"}
65
+ },
66
+ )
67
+
68
+ def _map_reason(self, reason: str) -> str:
69
+ r = (reason or "").upper()
70
+ if "TARGETING" in r or "RULE" in r:
71
+ return "TARGETING_MATCH"
72
+ if "PERCENTAGE" in r or "MULTIVARIATE" in r or "BUCKET" in r:
73
+ return "SPLIT"
74
+ if "KILL_SWITCH" in r or "ENV_DISABLED" in r or "DISABLED" in r:
75
+ return "DISABLED"
76
+ if "DEFAULT" in r:
77
+ return "DEFAULT"
78
+ return "STATIC"
79
+
80
+ def resolve_boolean_details(
81
+ self, flag_key: str, default_value: bool, evaluation_context: Any = None
82
+ ) -> ResolutionDetails:
83
+ try:
84
+ ctx = self._map_context(evaluation_context)
85
+ res = self.client.evaluate(flag_key, ctx)
86
+
87
+ if res.reason == "FLAG_NOT_FOUND":
88
+ return ResolutionDetails(
89
+ value=default_value,
90
+ reason="ERROR",
91
+ error_code="FLAG_NOT_FOUND",
92
+ error_message=f"Flag '{flag_key}' not found",
93
+ )
94
+
95
+ if not res.enabled:
96
+ return ResolutionDetails(
97
+ value=default_value,
98
+ variant=res.variant or "off",
99
+ reason=self._map_reason(res.reason),
100
+ )
101
+
102
+ val = res.value if isinstance(res.value, bool) else bool(res.value) if res.value is not None else True
103
+ return ResolutionDetails(
104
+ value=val,
105
+ variant=res.variant or "treatment",
106
+ reason=self._map_reason(res.reason),
107
+ )
108
+ except Exception as e:
109
+ return ResolutionDetails(
110
+ value=default_value,
111
+ reason="ERROR",
112
+ error_code="GENERAL",
113
+ error_message=str(e),
114
+ )
115
+
116
+ def resolve_string_details(
117
+ self, flag_key: str, default_value: str, evaluation_context: Any = None
118
+ ) -> ResolutionDetails:
119
+ try:
120
+ ctx = self._map_context(evaluation_context)
121
+ res = self.client.evaluate(flag_key, ctx)
122
+
123
+ if res.reason == "FLAG_NOT_FOUND":
124
+ return ResolutionDetails(
125
+ value=default_value,
126
+ reason="ERROR",
127
+ error_code="FLAG_NOT_FOUND",
128
+ error_message=f"Flag '{flag_key}' not found",
129
+ )
130
+
131
+ if not res.enabled:
132
+ return ResolutionDetails(
133
+ value=default_value,
134
+ variant=res.variant or "off",
135
+ reason=self._map_reason(res.reason),
136
+ )
137
+
138
+ val = str(res.value) if res.value else res.variant or default_value
139
+ return ResolutionDetails(
140
+ value=val,
141
+ variant=res.variant or "treatment",
142
+ reason=self._map_reason(res.reason),
143
+ )
144
+ except Exception as e:
145
+ return ResolutionDetails(
146
+ value=default_value,
147
+ reason="ERROR",
148
+ error_code="GENERAL",
149
+ error_message=str(e),
150
+ )
151
+
152
+ def resolve_integer_details(
153
+ self, flag_key: str, default_value: int, evaluation_context: Any = None
154
+ ) -> ResolutionDetails:
155
+ try:
156
+ ctx = self._map_context(evaluation_context)
157
+ res = self.client.evaluate(flag_key, ctx)
158
+
159
+ if res.reason == "FLAG_NOT_FOUND":
160
+ return ResolutionDetails(
161
+ value=default_value,
162
+ reason="ERROR",
163
+ error_code="FLAG_NOT_FOUND",
164
+ error_message=f"Flag '{flag_key}' not found",
165
+ )
166
+
167
+ if not res.enabled:
168
+ return ResolutionDetails(
169
+ value=default_value,
170
+ variant=res.variant or "off",
171
+ reason=self._map_reason(res.reason),
172
+ )
173
+
174
+ try:
175
+ val = int(res.value)
176
+ except (ValueError, TypeError):
177
+ val = default_value
178
+
179
+ return ResolutionDetails(
180
+ value=val,
181
+ variant=res.variant or "treatment",
182
+ reason=self._map_reason(res.reason),
183
+ )
184
+ except Exception as e:
185
+ return ResolutionDetails(
186
+ value=default_value,
187
+ reason="ERROR",
188
+ error_code="GENERAL",
189
+ error_message=str(e),
190
+ )
191
+
192
+ def resolve_float_details(
193
+ self, flag_key: str, default_value: float, evaluation_context: Any = None
194
+ ) -> ResolutionDetails:
195
+ try:
196
+ ctx = self._map_context(evaluation_context)
197
+ res = self.client.evaluate(flag_key, ctx)
198
+
199
+ if res.reason == "FLAG_NOT_FOUND":
200
+ return ResolutionDetails(
201
+ value=default_value,
202
+ reason="ERROR",
203
+ error_code="FLAG_NOT_FOUND",
204
+ error_message=f"Flag '{flag_key}' not found",
205
+ )
206
+
207
+ if not res.enabled:
208
+ return ResolutionDetails(
209
+ value=default_value,
210
+ variant=res.variant or "off",
211
+ reason=self._map_reason(res.reason),
212
+ )
213
+
214
+ try:
215
+ val = float(res.value)
216
+ except (ValueError, TypeError):
217
+ val = default_value
218
+
219
+ return ResolutionDetails(
220
+ value=val,
221
+ variant=res.variant or "treatment",
222
+ reason=self._map_reason(res.reason),
223
+ )
224
+ except Exception as e:
225
+ return ResolutionDetails(
226
+ value=default_value,
227
+ reason="ERROR",
228
+ error_code="GENERAL",
229
+ error_message=str(e),
230
+ )
231
+
232
+ def resolve_object_details(
233
+ self, flag_key: str, default_value: Any, evaluation_context: Any = None
234
+ ) -> ResolutionDetails:
235
+ try:
236
+ ctx = self._map_context(evaluation_context)
237
+ res = self.client.evaluate(flag_key, ctx)
238
+
239
+ if res.reason == "FLAG_NOT_FOUND":
240
+ return ResolutionDetails(
241
+ value=default_value,
242
+ reason="ERROR",
243
+ error_code="FLAG_NOT_FOUND",
244
+ error_message=f"Flag '{flag_key}' not found",
245
+ )
246
+
247
+ if not res.enabled:
248
+ return ResolutionDetails(
249
+ value=default_value,
250
+ variant=res.variant or "off",
251
+ reason=self._map_reason(res.reason),
252
+ )
253
+
254
+ val = res.value if res.value is not None else default_value
255
+ return ResolutionDetails(
256
+ value=val,
257
+ variant=res.variant or "treatment",
258
+ reason=self._map_reason(res.reason),
259
+ )
260
+ except Exception as e:
261
+ return ResolutionDetails(
262
+ value=default_value,
263
+ reason="ERROR",
264
+ error_code="GENERAL",
265
+ error_message=str(e),
266
+ )
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: flagura-sdk
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK and OpenFeature Provider for Flagura
5
+ Author-email: Dhawal Dyavanpalli <dhawalhost@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dhawalhost/flagura
8
+ Project-URL: Repository, https://github.com/dhawalhost/flagura
9
+ Keywords: feature-flags,feature-toggles,openfeature,flagura,a-b-testing
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: requests>=2.28.0
13
+ Requires-Dist: openfeature-sdk>=0.7.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
16
+ Requires-Dist: build>=1.0.0; extra == "dev"
17
+ Requires-Dist: twine>=4.0.0; extra == "dev"
18
+
19
+ # ⚡ Flagura Python SDK
20
+
21
+ Official Python client for the **Flagura Feature Flag Platform**, supporting:
22
+ - High-performance evaluations
23
+ - **Real-Time SSE Flag Streaming (`<5ms` sync)**
24
+ - **CNCF OpenFeature Provider**
25
+ - FastAPI, Django, Flask, Celery, and AI Agent compatibility
26
+
27
+ ---
28
+
29
+ ## 📦 Installation
30
+
31
+ ```bash
32
+ pip install flagura
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🚀 Quickstart
38
+
39
+ ### 1. Direct Client with Real-Time SSE Streaming
40
+
41
+ ```python
42
+ from flagura import FlaguraClient, EvaluationContext
43
+
44
+ # Initialize client with real-time SSE streaming
45
+ client = FlaguraClient(
46
+ endpoint="https://flagura.dhawalhost.com",
47
+ api_key="your-api-key",
48
+ project_id="proj_default", # optional: project scoping
49
+ default_environment="production",
50
+ enable_streaming=True, # <5ms live flag updates
51
+ )
52
+
53
+ # Register real-time change listener
54
+ client.on_update(lambda flags: print(f"Flags updated in real-time! Count: {len(flags)}"))
55
+
56
+ # Evaluate flag
57
+ context = EvaluationContext(
58
+ user_id="usr_dhawal_01",
59
+ email="dhawal@flagura.dev",
60
+ tier="enterprise",
61
+ )
62
+
63
+ if client.is_enabled("ai-smart-search", context):
64
+ variant = client.get_variant("ai-smart-search", context)
65
+ print(f"AI Smart Search is ON! Variant: {variant}")
66
+
67
+ # Cleanup on shutdown
68
+ client.close()
69
+ ```
70
+
71
+ ---
72
+
73
+ ### 2. OpenFeature Universal Provider
74
+
75
+ ```python
76
+ from openfeature import api
77
+ from openfeature.evaluation_context import EvaluationContext
78
+ from flagura.openfeature_provider import FlaguraOpenFeatureProvider
79
+
80
+ # Register Flagura as OpenFeature provider
81
+ api.set_provider(FlaguraOpenFeatureProvider(
82
+ endpoint="https://flagura.dhawalhost.com",
83
+ api_key="your-api-key",
84
+ enable_streaming=True,
85
+ ))
86
+ of_client = api.get_client()
87
+
88
+ # Evaluate with OpenFeature standard APIs
89
+ ctx = EvaluationContext(targeting_key="usr_dhawal_01", attributes={"email": "dhawal@flagura.dev"})
90
+ is_enabled = of_client.get_boolean_value("ai-smart-search", False, ctx)
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 📄 License
96
+ MIT
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ flagura/__init__.py
4
+ flagura/client.py
5
+ flagura/openfeature_provider.py
6
+ flagura_sdk.egg-info/PKG-INFO
7
+ flagura_sdk.egg-info/SOURCES.txt
8
+ flagura_sdk.egg-info/dependency_links.txt
9
+ flagura_sdk.egg-info/requires.txt
10
+ flagura_sdk.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ requests>=2.28.0
2
+ openfeature-sdk>=0.7.0
3
+
4
+ [dev]
5
+ pytest>=7.0.0
6
+ build>=1.0.0
7
+ twine>=4.0.0
@@ -0,0 +1 @@
1
+ flagura
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flagura-sdk"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK and OpenFeature Provider for Flagura"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Dhawal Dyavanpalli", email = "dhawalhost@gmail.com" }
14
+ ]
15
+ keywords = ["feature-flags", "feature-toggles", "openfeature", "flagura", "a-b-testing"]
16
+ dependencies = [
17
+ "requests>=2.28.0",
18
+ "openfeature-sdk>=0.7.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest>=7.0.0",
24
+ "build>=1.0.0",
25
+ "twine>=4.0.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/dhawalhost/flagura"
30
+ Repository = "https://github.com/dhawalhost/flagura"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+