flagkit 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.
- flagkit-1.0.0/.gitignore +75 -0
- flagkit-1.0.0/PKG-INFO +585 -0
- flagkit-1.0.0/README.md +551 -0
- flagkit-1.0.0/pyproject.toml +122 -0
- flagkit-1.0.0/src/flagkit/__init__.py +66 -0
- flagkit-1.0.0/src/flagkit/client.py +756 -0
- flagkit-1.0.0/src/flagkit/core/__init__.py +35 -0
- flagkit-1.0.0/src/flagkit/core/cache.py +333 -0
- flagkit-1.0.0/src/flagkit/core/context_manager.py +122 -0
- flagkit-1.0.0/src/flagkit/core/event_persistence.py +621 -0
- flagkit-1.0.0/src/flagkit/core/event_queue.py +417 -0
- flagkit-1.0.0/src/flagkit/core/polling_manager.py +190 -0
- flagkit-1.0.0/src/flagkit/core/streaming_manager.py +530 -0
- flagkit-1.0.0/src/flagkit/errors/__init__.py +52 -0
- flagkit-1.0.0/src/flagkit/errors/error_codes.py +527 -0
- flagkit-1.0.0/src/flagkit/errors/flagkit_error.py +288 -0
- flagkit-1.0.0/src/flagkit/errors/sanitizer.py +98 -0
- flagkit-1.0.0/src/flagkit/flagkit.py +278 -0
- flagkit-1.0.0/src/flagkit/http/__init__.py +44 -0
- flagkit-1.0.0/src/flagkit/http/circuit_breaker.py +191 -0
- flagkit-1.0.0/src/flagkit/http/http_client.py +552 -0
- flagkit-1.0.0/src/flagkit/http/retry.py +254 -0
- flagkit-1.0.0/src/flagkit/py.typed +0 -0
- flagkit-1.0.0/src/flagkit/storage/__init__.py +11 -0
- flagkit-1.0.0/src/flagkit/storage/encrypted_storage.py +322 -0
- flagkit-1.0.0/src/flagkit/storage/memory_storage.py +57 -0
- flagkit-1.0.0/src/flagkit/storage/storage.py +45 -0
- flagkit-1.0.0/src/flagkit/types/__init__.py +51 -0
- flagkit-1.0.0/src/flagkit/types/config.py +198 -0
- flagkit-1.0.0/src/flagkit/types/context.py +154 -0
- flagkit-1.0.0/src/flagkit/types/events.py +101 -0
- flagkit-1.0.0/src/flagkit/types/flag.py +192 -0
- flagkit-1.0.0/src/flagkit/utils/__init__.py +86 -0
- flagkit-1.0.0/src/flagkit/utils/logger.py +120 -0
- flagkit-1.0.0/src/flagkit/utils/platform.py +67 -0
- flagkit-1.0.0/src/flagkit/utils/security.py +548 -0
- flagkit-1.0.0/src/flagkit/utils/validators.py +193 -0
- flagkit-1.0.0/src/flagkit/utils/version.py +126 -0
flagkit-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Environments
|
|
54
|
+
.env
|
|
55
|
+
.venv
|
|
56
|
+
env/
|
|
57
|
+
venv/
|
|
58
|
+
ENV/
|
|
59
|
+
env.bak/
|
|
60
|
+
venv.bak/
|
|
61
|
+
|
|
62
|
+
# mypy
|
|
63
|
+
.mypy_cache/
|
|
64
|
+
.dmypy.json
|
|
65
|
+
dmypy.json
|
|
66
|
+
|
|
67
|
+
# IDEs
|
|
68
|
+
.idea/
|
|
69
|
+
.vscode/
|
|
70
|
+
*.swp
|
|
71
|
+
*.swo
|
|
72
|
+
|
|
73
|
+
# OS
|
|
74
|
+
.DS_Store
|
|
75
|
+
Thumbs.db
|
flagkit-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flagkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: FlagKit SDK for Python - Feature flag evaluation and analytics
|
|
5
|
+
Project-URL: Homepage, https://flagkit.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.flagkit.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/teracrafts/flagkit-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/teracrafts/flagkit-sdk/issues
|
|
9
|
+
Author-email: FlagKit <sdk@flagkit.dev>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: feature-flags,feature-toggles,flagkit,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: cryptography>=41.0.0
|
|
25
|
+
Requires-Dist: httpx>=0.25.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-httpx>=0.28.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1.9; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# FlagKit Python SDK
|
|
36
|
+
|
|
37
|
+
Official Python SDK for [FlagKit](https://flagkit.dev) - Feature flag management made simple.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install flagkit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Python 3.9+
|
|
48
|
+
- `httpx` (automatically installed)
|
|
49
|
+
- `cryptography` (automatically installed, for cache encryption)
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from flagkit import FlagKit
|
|
55
|
+
|
|
56
|
+
# Initialize the SDK
|
|
57
|
+
client = FlagKit.initialize(
|
|
58
|
+
api_key="sdk_your_api_key",
|
|
59
|
+
polling_interval=30, # seconds
|
|
60
|
+
cache_ttl=300, # seconds
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Wait for initialization
|
|
64
|
+
client.wait_for_ready()
|
|
65
|
+
|
|
66
|
+
# Identify a user
|
|
67
|
+
client.identify("user-123", {"plan": "premium"})
|
|
68
|
+
|
|
69
|
+
# Evaluate flags
|
|
70
|
+
dark_mode = client.get_boolean_value("dark-mode", default=False)
|
|
71
|
+
welcome_message = client.get_string_value("welcome-message", default="Hello!")
|
|
72
|
+
max_items = client.get_number_value("max-items", default=10)
|
|
73
|
+
config = client.get_json_value("feature-config", default={})
|
|
74
|
+
|
|
75
|
+
# Get full evaluation details
|
|
76
|
+
result = client.evaluate("dark-mode")
|
|
77
|
+
print(f"Value: {result.value}, Reason: {result.reason}")
|
|
78
|
+
|
|
79
|
+
# Track custom events
|
|
80
|
+
client.track("button_clicked", {"button": "signup"})
|
|
81
|
+
|
|
82
|
+
# Cleanup when done
|
|
83
|
+
client.close()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Features
|
|
87
|
+
|
|
88
|
+
- **Type-safe evaluation** - Boolean, string, number, and JSON flag types
|
|
89
|
+
- **Local caching** - Fast evaluations with configurable TTL and optional encryption
|
|
90
|
+
- **Background polling** - Automatic flag updates with jitter
|
|
91
|
+
- **Event tracking** - Analytics with batching and crash-resilient persistence
|
|
92
|
+
- **Resilient** - Circuit breaker, retry with exponential backoff, offline support
|
|
93
|
+
- **Thread-safe** - Safe for concurrent use
|
|
94
|
+
- **Security** - PII detection, request signing, bootstrap verification, timing attack protection
|
|
95
|
+
|
|
96
|
+
## Architecture
|
|
97
|
+
|
|
98
|
+
The SDK is organized into clean, modular packages:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
flagkit/
|
|
102
|
+
├── __init__.py # Public exports
|
|
103
|
+
├── flagkit.py # Static FlagKit factory (singleton)
|
|
104
|
+
├── client.py # FlagKitClient implementation
|
|
105
|
+
├── types/ # Public type definitions
|
|
106
|
+
│ ├── config.py # FlagKitOptions, BootstrapConfig
|
|
107
|
+
│ ├── context.py # EvaluationContext
|
|
108
|
+
│ ├── flag.py # FlagState, EvaluationResult
|
|
109
|
+
│ └── events.py # Event types
|
|
110
|
+
├── errors/ # Error types and codes
|
|
111
|
+
│ ├── error_codes.py # 31 error codes
|
|
112
|
+
│ ├── flagkit_error.py # Error hierarchy
|
|
113
|
+
│ └── sanitizer.py # Error message sanitization
|
|
114
|
+
├── http/ # HTTP client, circuit breaker, retry
|
|
115
|
+
│ ├── http_client.py # HTTP with retry/circuit integration
|
|
116
|
+
│ ├── circuit_breaker.py
|
|
117
|
+
│ └── retry.py # Exponential backoff with jitter
|
|
118
|
+
├── core/ # Core components
|
|
119
|
+
│ ├── cache.py # In-memory cache with TTL
|
|
120
|
+
│ ├── context_manager.py
|
|
121
|
+
│ ├── polling_manager.py # Background polling
|
|
122
|
+
│ ├── event_queue.py # Event batching
|
|
123
|
+
│ └── event_persistence.py # Crash-resilient persistence
|
|
124
|
+
├── storage/ # Storage implementations
|
|
125
|
+
│ ├── storage.py # Storage Protocol
|
|
126
|
+
│ ├── memory_storage.py
|
|
127
|
+
│ └── encrypted_storage.py # AES-256-GCM encryption
|
|
128
|
+
└── utils/ # Utilities
|
|
129
|
+
├── logger.py # Logger interface
|
|
130
|
+
├── security.py # PII detection, HMAC signing
|
|
131
|
+
├── platform.py # Platform detection
|
|
132
|
+
└── validators.py # Input validation
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## API Reference
|
|
136
|
+
|
|
137
|
+
### Initialization
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from flagkit import FlagKit, FlagKitClient, FlagKitOptions
|
|
141
|
+
|
|
142
|
+
# Using the static factory (recommended for single client)
|
|
143
|
+
client = FlagKit.initialize(
|
|
144
|
+
api_key="sdk_...", # Required
|
|
145
|
+
base_url="https://api.flagkit.dev/api/v1", # Optional
|
|
146
|
+
polling_interval=30.0, # Seconds between polls
|
|
147
|
+
enable_polling=True, # Enable background polling
|
|
148
|
+
cache_enabled=True, # Enable local caching
|
|
149
|
+
cache_ttl=300.0, # Cache TTL in seconds
|
|
150
|
+
offline=False, # Offline mode
|
|
151
|
+
timeout=5.0, # Request timeout in seconds
|
|
152
|
+
retries=3, # Number of retries
|
|
153
|
+
bootstrap={"flag": True}, # Initial flag values
|
|
154
|
+
debug=False, # Enable debug logging
|
|
155
|
+
on_ready=lambda: print("Ready!"),
|
|
156
|
+
on_error=lambda e: print(f"Error: {e}"),
|
|
157
|
+
on_update=lambda flags: print(f"Updated: {len(flags)} flags"),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Or create a client directly
|
|
161
|
+
options = FlagKitOptions(api_key="sdk_...")
|
|
162
|
+
client = FlagKitClient(options)
|
|
163
|
+
client.initialize()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Flag Evaluation
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# Boolean flags
|
|
170
|
+
enabled = client.get_boolean_value("feature-flag", default=False)
|
|
171
|
+
|
|
172
|
+
# String flags
|
|
173
|
+
variant = client.get_string_value("button-text", default="Click")
|
|
174
|
+
|
|
175
|
+
# Number flags
|
|
176
|
+
limit = client.get_number_value("rate-limit", default=100)
|
|
177
|
+
|
|
178
|
+
# JSON flags
|
|
179
|
+
config = client.get_json_value("config", default={"enabled": False})
|
|
180
|
+
|
|
181
|
+
# Full evaluation result
|
|
182
|
+
result = client.evaluate("feature-flag")
|
|
183
|
+
# result.flag_key, result.value, result.enabled, result.reason, result.version
|
|
184
|
+
|
|
185
|
+
# Evaluate all flags
|
|
186
|
+
all_results = client.evaluate_all()
|
|
187
|
+
|
|
188
|
+
# Check flag existence
|
|
189
|
+
if client.has_flag("my-flag"):
|
|
190
|
+
# ...
|
|
191
|
+
|
|
192
|
+
# Get all flag keys
|
|
193
|
+
keys = client.get_all_flag_keys()
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Context Management
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from flagkit import EvaluationContext
|
|
200
|
+
|
|
201
|
+
# Set global context
|
|
202
|
+
context = EvaluationContext(
|
|
203
|
+
user_id="user-123",
|
|
204
|
+
email="user@example.com",
|
|
205
|
+
country="US",
|
|
206
|
+
custom={"plan": "premium", "beta_tester": True},
|
|
207
|
+
private_attributes=["email"], # Not sent to server
|
|
208
|
+
)
|
|
209
|
+
client.set_context(context)
|
|
210
|
+
|
|
211
|
+
# Get current context
|
|
212
|
+
current = client.get_context()
|
|
213
|
+
|
|
214
|
+
# Clear context
|
|
215
|
+
client.clear_context()
|
|
216
|
+
|
|
217
|
+
# Identify user (shorthand)
|
|
218
|
+
client.identify("user-123", {"email": "user@example.com"})
|
|
219
|
+
|
|
220
|
+
# Reset to anonymous
|
|
221
|
+
client.reset()
|
|
222
|
+
|
|
223
|
+
# Pass context to evaluation
|
|
224
|
+
result = client.get_boolean_value(
|
|
225
|
+
"feature-flag",
|
|
226
|
+
default=False,
|
|
227
|
+
context=EvaluationContext(user_id="other-user"),
|
|
228
|
+
)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Event Tracking
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Track custom event
|
|
235
|
+
client.track("purchase", {
|
|
236
|
+
"amount": 99.99,
|
|
237
|
+
"currency": "USD",
|
|
238
|
+
"product_id": "prod-123",
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
# Force flush pending events
|
|
242
|
+
client.flush()
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Lifecycle
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Check if SDK is ready
|
|
249
|
+
if client.is_ready():
|
|
250
|
+
# ...
|
|
251
|
+
|
|
252
|
+
# Wait for ready (blocks until initialized)
|
|
253
|
+
client.wait_for_ready()
|
|
254
|
+
|
|
255
|
+
# Force refresh flags from server
|
|
256
|
+
client.refresh()
|
|
257
|
+
|
|
258
|
+
# Close SDK and cleanup
|
|
259
|
+
client.close()
|
|
260
|
+
|
|
261
|
+
# Using context manager
|
|
262
|
+
with FlagKitClient(options) as client:
|
|
263
|
+
client.initialize()
|
|
264
|
+
# Use client...
|
|
265
|
+
# Automatically closed
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Error Handling
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
from flagkit import FlagKitError, InitializationError, NetworkError
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
client = FlagKit.initialize(api_key="sdk_...")
|
|
275
|
+
except InitializationError as e:
|
|
276
|
+
print(f"Failed to initialize: {e.code} - {e}")
|
|
277
|
+
except NetworkError as e:
|
|
278
|
+
if e.recoverable:
|
|
279
|
+
# Retry logic
|
|
280
|
+
pass
|
|
281
|
+
except FlagKitError as e:
|
|
282
|
+
print(f"Error [{e.code}]: {e}")
|
|
283
|
+
print(f"Recoverable: {e.recoverable}")
|
|
284
|
+
print(f"Details: {e.to_dict()}")
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Local Development
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Connect to local FlagKit server at http://localhost:8200/api/v1
|
|
291
|
+
client = FlagKit.initialize(
|
|
292
|
+
api_key="sdk_...",
|
|
293
|
+
local_port=8200,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Or use a custom port
|
|
297
|
+
client = FlagKit.initialize(
|
|
298
|
+
api_key="sdk_...",
|
|
299
|
+
local_port=3000, # Uses http://localhost:3000/api/v1
|
|
300
|
+
)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Offline Mode
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
# Start in offline mode
|
|
307
|
+
client = FlagKit.initialize(
|
|
308
|
+
api_key="sdk_...",
|
|
309
|
+
offline=True,
|
|
310
|
+
bootstrap={"feature-flag": True},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Uses bootstrap values without network requests
|
|
314
|
+
value = client.get_boolean_value("feature-flag", default=False)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Security Features
|
|
318
|
+
|
|
319
|
+
### PII Detection
|
|
320
|
+
|
|
321
|
+
The SDK can detect and warn about potential PII (Personally Identifiable Information) in contexts and events:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# Enable strict PII mode - raises errors instead of warnings
|
|
325
|
+
client = FlagKit.initialize(
|
|
326
|
+
api_key="sdk_...",
|
|
327
|
+
strict_pii_mode=True,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Attributes containing PII will raise SecurityError
|
|
331
|
+
try:
|
|
332
|
+
client.identify("user-123", {"email": "user@example.com"}) # PII detected!
|
|
333
|
+
except SecurityError as e:
|
|
334
|
+
print(f"PII error: {e}")
|
|
335
|
+
|
|
336
|
+
# Use private attributes to mark fields as intentionally containing PII
|
|
337
|
+
context = EvaluationContext(
|
|
338
|
+
user_id="user-123",
|
|
339
|
+
email="user@example.com",
|
|
340
|
+
private_attributes=["email"], # Marks email as intentionally private
|
|
341
|
+
)
|
|
342
|
+
client.set_context(context) # No error - email marked as private
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Request Signing
|
|
346
|
+
|
|
347
|
+
POST requests to the FlagKit API are signed with HMAC-SHA256 for integrity:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
# Enabled by default, can be disabled if needed
|
|
351
|
+
client = FlagKit.initialize(
|
|
352
|
+
api_key="sdk_...",
|
|
353
|
+
enable_request_signing=False, # Disable signing
|
|
354
|
+
)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Bootstrap Signature Verification
|
|
358
|
+
|
|
359
|
+
Verify bootstrap data integrity using HMAC signatures:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from flagkit import BootstrapConfig, BootstrapVerificationOptions
|
|
363
|
+
from flagkit.utils.security import create_bootstrap_signature
|
|
364
|
+
|
|
365
|
+
# Create signed bootstrap data
|
|
366
|
+
bootstrap = create_bootstrap_signature(
|
|
367
|
+
flags={"feature-a": True, "feature-b": "value"},
|
|
368
|
+
api_key="sdk_your_api_key",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Use signed bootstrap with verification
|
|
372
|
+
client = FlagKit.initialize(
|
|
373
|
+
api_key="sdk_...",
|
|
374
|
+
bootstrap=bootstrap,
|
|
375
|
+
bootstrap_verification=BootstrapVerificationOptions(
|
|
376
|
+
enabled=True,
|
|
377
|
+
max_age=86400000, # 24 hours in milliseconds
|
|
378
|
+
on_failure="error", # "warn" (default), "error", or "ignore"
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Cache Encryption
|
|
384
|
+
|
|
385
|
+
Enable AES-256-GCM encryption for cached flag data:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
client = FlagKit.initialize(
|
|
389
|
+
api_key="sdk_...",
|
|
390
|
+
encrypt_cache=True,
|
|
391
|
+
)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Evaluation Jitter (Timing Attack Protection)
|
|
395
|
+
|
|
396
|
+
Add random delays to flag evaluations to prevent cache timing attacks:
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
from flagkit import EvaluationJitterConfig
|
|
400
|
+
|
|
401
|
+
client = FlagKit.initialize(
|
|
402
|
+
api_key="sdk_...",
|
|
403
|
+
evaluation_jitter=EvaluationJitterConfig(
|
|
404
|
+
enabled=True,
|
|
405
|
+
min_ms=5,
|
|
406
|
+
max_ms=15,
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Error Sanitization
|
|
412
|
+
|
|
413
|
+
Automatically redact sensitive information from error messages:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
from flagkit import ErrorSanitizationConfig
|
|
417
|
+
|
|
418
|
+
client = FlagKit.initialize(
|
|
419
|
+
api_key="sdk_...",
|
|
420
|
+
error_sanitization=ErrorSanitizationConfig(
|
|
421
|
+
enabled=True,
|
|
422
|
+
preserve_original=False, # Set True to keep original for debugging
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
# Errors will have paths, IPs, API keys, and emails redacted
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Event Persistence
|
|
429
|
+
|
|
430
|
+
Enable crash-resilient event persistence to prevent data loss:
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
client = FlagKit.initialize(
|
|
434
|
+
api_key="sdk_...",
|
|
435
|
+
persist_events=True,
|
|
436
|
+
event_storage_path="/path/to/storage", # Optional, defaults to temp dir
|
|
437
|
+
max_persisted_events=10000, # Optional, default 10000
|
|
438
|
+
persistence_flush_interval=1000, # Optional, default 1000ms
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Events are written to disk before being sent, and automatically recovered on restart.
|
|
443
|
+
|
|
444
|
+
## Key Rotation
|
|
445
|
+
|
|
446
|
+
Support seamless API key rotation:
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
client = FlagKit.initialize(
|
|
450
|
+
api_key="sdk_primary_key",
|
|
451
|
+
secondary_api_key="sdk_secondary_key",
|
|
452
|
+
key_rotation_grace_period=300.0, # 5 minutes
|
|
453
|
+
)
|
|
454
|
+
# SDK will automatically failover to secondary key on 401 errors
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Error Handling
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
from flagkit import (
|
|
461
|
+
FlagKitError,
|
|
462
|
+
InitializationError,
|
|
463
|
+
NetworkError,
|
|
464
|
+
SecurityError,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
client = FlagKit.initialize(api_key="sdk_...")
|
|
469
|
+
except InitializationError as e:
|
|
470
|
+
print(f"Failed to initialize: {e.code} - {e}")
|
|
471
|
+
except SecurityError as e:
|
|
472
|
+
print(f"Security error: {e.code} - {e}")
|
|
473
|
+
except NetworkError as e:
|
|
474
|
+
if e.recoverable:
|
|
475
|
+
# Retry logic
|
|
476
|
+
pass
|
|
477
|
+
except FlagKitError as e:
|
|
478
|
+
print(f"Error [{e.code}]: {e}")
|
|
479
|
+
print(f"Recoverable: {e.recoverable}")
|
|
480
|
+
print(f"Details: {e.to_dict()}")
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Error Codes
|
|
484
|
+
|
|
485
|
+
| Code | Description |
|
|
486
|
+
|------|-------------|
|
|
487
|
+
| `INIT_FAILED` | SDK initialization failed |
|
|
488
|
+
| `INIT_TIMEOUT` | Initialization timed out |
|
|
489
|
+
| `INIT_ALREADY_INITIALIZED` | SDK already initialized |
|
|
490
|
+
| `INIT_NOT_INITIALIZED` | SDK not initialized |
|
|
491
|
+
| `NETWORK_ERROR` | Network request failed |
|
|
492
|
+
| `AUTH_INVALID_KEY` | Invalid API key |
|
|
493
|
+
| `SECURITY_PII_DETECTED` | PII detected in strict mode |
|
|
494
|
+
| `SECURITY_LOCAL_PORT_IN_PRODUCTION` | Local port used in production |
|
|
495
|
+
| `SECURITY_SIGNATURE_INVALID` | Bootstrap signature verification failed |
|
|
496
|
+
|
|
497
|
+
## Configuration Options
|
|
498
|
+
|
|
499
|
+
| Option | Type | Default | Description |
|
|
500
|
+
|--------|------|---------|-------------|
|
|
501
|
+
| `api_key` | str | Required | API key for authentication |
|
|
502
|
+
| `secondary_api_key` | str | None | Secondary key for rotation |
|
|
503
|
+
| `key_rotation_grace_period` | float | 300.0 | Grace period in seconds |
|
|
504
|
+
| `base_url` | str | `https://api.flagkit.dev/api/v1` | API base URL |
|
|
505
|
+
| `local_port` | int | None | Local development port |
|
|
506
|
+
| `polling_interval` | float | 30.0 | Polling interval in seconds |
|
|
507
|
+
| `enable_polling` | bool | True | Enable background polling |
|
|
508
|
+
| `cache_enabled` | bool | True | Enable local caching |
|
|
509
|
+
| `cache_ttl` | float | 300.0 | Cache TTL in seconds |
|
|
510
|
+
| `encrypt_cache` | bool | False | Enable AES-256-GCM cache encryption |
|
|
511
|
+
| `offline` | bool | False | Offline mode |
|
|
512
|
+
| `timeout` | float | 5.0 | Request timeout in seconds |
|
|
513
|
+
| `retries` | int | 3 | Number of retry attempts |
|
|
514
|
+
| `bootstrap` | dict/BootstrapConfig | {} | Initial flag values |
|
|
515
|
+
| `bootstrap_verification` | BootstrapVerificationOptions | enabled | Bootstrap verification settings |
|
|
516
|
+
| `debug` | bool | False | Enable debug logging |
|
|
517
|
+
| `logger` | LoggerProtocol | None | Custom logger |
|
|
518
|
+
| `on_ready` | Callable | None | Ready callback |
|
|
519
|
+
| `on_error` | Callable | None | Error callback |
|
|
520
|
+
| `on_update` | Callable | None | Update callback |
|
|
521
|
+
| `enable_request_signing` | bool | True | Enable request signing |
|
|
522
|
+
| `strict_pii_mode` | bool | False | Error on PII detection |
|
|
523
|
+
| `persist_events` | bool | False | Enable event persistence |
|
|
524
|
+
| `event_storage_path` | str | temp dir | Event storage directory |
|
|
525
|
+
| `max_persisted_events` | int | 10000 | Max persisted events |
|
|
526
|
+
| `persistence_flush_interval` | int | 1000 | Persistence flush interval (ms) |
|
|
527
|
+
| `evaluation_jitter` | EvaluationJitterConfig | disabled | Timing attack protection |
|
|
528
|
+
| `error_sanitization` | ErrorSanitizationConfig | enabled | Redact sensitive info from errors |
|
|
529
|
+
|
|
530
|
+
## Testing
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
# Use offline mode with bootstrap values
|
|
534
|
+
client = FlagKit.initialize(
|
|
535
|
+
api_key="sdk_test",
|
|
536
|
+
offline=True,
|
|
537
|
+
bootstrap={"feature-flag": True},
|
|
538
|
+
)
|
|
539
|
+
client.wait_for_ready()
|
|
540
|
+
|
|
541
|
+
# With signed bootstrap for verification testing
|
|
542
|
+
from flagkit.utils.security import create_bootstrap_signature
|
|
543
|
+
|
|
544
|
+
bootstrap = create_bootstrap_signature(
|
|
545
|
+
flags={"feature-flag": True},
|
|
546
|
+
api_key="sdk_test",
|
|
547
|
+
)
|
|
548
|
+
client = FlagKit.initialize(
|
|
549
|
+
api_key="sdk_test",
|
|
550
|
+
offline=True,
|
|
551
|
+
bootstrap=bootstrap,
|
|
552
|
+
)
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Thread Safety
|
|
556
|
+
|
|
557
|
+
All SDK methods are safe for concurrent use from multiple threads. The client uses internal synchronization (threading.Lock) to ensure thread-safe access to:
|
|
558
|
+
|
|
559
|
+
- Flag cache
|
|
560
|
+
- Event queue
|
|
561
|
+
- Context management
|
|
562
|
+
- Polling state
|
|
563
|
+
|
|
564
|
+
## Development
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
# Install dev dependencies
|
|
568
|
+
pip install -e ".[dev]"
|
|
569
|
+
|
|
570
|
+
# Run tests
|
|
571
|
+
pytest
|
|
572
|
+
|
|
573
|
+
# Run with coverage
|
|
574
|
+
pytest --cov
|
|
575
|
+
|
|
576
|
+
# Type checking
|
|
577
|
+
mypy src/
|
|
578
|
+
|
|
579
|
+
# Linting
|
|
580
|
+
ruff check src/
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## License
|
|
584
|
+
|
|
585
|
+
MIT License - see [LICENSE](LICENSE) for details.
|