variably-sdk 2.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,516 @@
1
+ Metadata-Version: 2.4
2
+ Name: variably-sdk
3
+ Version: 2.0.0
4
+ Summary: Official Python SDK for Variably feature flags, LLM experimentation, and prompt optimization platform
5
+ Author: Variably
6
+ Author-email: Variably <support@variably.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/variably/variably-python-sdk
9
+ Project-URL: Documentation, https://docs.variably.com/sdks/python
10
+ Project-URL: Repository, https://github.com/variably/variably-python-sdk
11
+ Project-URL: Issues, https://github.com/variably/variably-python-sdk/issues
12
+ Keywords: feature-flags,experimentation,a-b-testing,variably,llm,prompt-experimentation,llmops
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: requests>=2.25.0
29
+ Requires-Dist: typing-extensions>=3.7.4; python_version < "3.8"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=6.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio>=0.18.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=2.10; extra == "dev"
34
+ Requires-Dist: black>=21.0; extra == "dev"
35
+ Requires-Dist: flake8>=3.8; extra == "dev"
36
+ Requires-Dist: mypy>=0.800; extra == "dev"
37
+ Requires-Dist: isort>=5.0; extra == "dev"
38
+ Requires-Dist: responses>=0.18.0; extra == "dev"
39
+ Provides-Extra: test
40
+ Requires-Dist: pytest>=6.0; extra == "test"
41
+ Requires-Dist: pytest-cov>=2.10; extra == "test"
42
+ Requires-Dist: responses>=0.18.0; extra == "test"
43
+ Dynamic: author
44
+ Dynamic: requires-python
45
+
46
+ # Variably Python SDK
47
+
48
+ Official Python SDK for Variably — feature flags, LLM experimentation, and prompt optimization.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install variably-sdk
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ from variably import VariablyClient
60
+
61
+ # Initialize the client
62
+ client = VariablyClient({
63
+ "api_key": "your-api-key",
64
+ "base_url": "https://api.variably.com", # optional, defaults to localhost:8080
65
+ "environment": "production" # optional
66
+ })
67
+
68
+ # Evaluate a boolean feature flag
69
+ user_context = {
70
+ "user_id": "user-123",
71
+ "email": "user@example.com",
72
+ "country": "US"
73
+ }
74
+
75
+ is_feature_enabled = client.evaluate_flag_bool(
76
+ "new-checkout-flow",
77
+ False, # default value
78
+ user_context
79
+ )
80
+
81
+ if is_feature_enabled:
82
+ # Show new checkout flow
83
+ pass
84
+
85
+ # Evaluate a feature gate
86
+ has_access = client.evaluate_gate("premium-features", user_context)
87
+
88
+ # Track events
89
+ client.track({
90
+ "name": "button_clicked",
91
+ "user_id": "user-123",
92
+ "properties": {
93
+ "button_name": "checkout",
94
+ "page": "product-detail"
95
+ }
96
+ })
97
+
98
+ # Clean up resources
99
+ client.close()
100
+ ```
101
+
102
+ ## Prompt Experimentation
103
+
104
+ Variably provides two modes for LLM prompt experimentation:
105
+
106
+ ### BYOR (Bring Your Own Runtime)
107
+
108
+ You call your own LLM. Variably handles variant allocation and 41-dimensional evaluation.
109
+
110
+ ```python
111
+ from variably import VariablyClient
112
+ import time
113
+
114
+ client = VariablyClient({"api_key": "your-api-key"})
115
+
116
+ user_context = {"user_id": "user-123"}
117
+ input_variables = {"query": "What are the symptoms of Type 2 diabetes?"}
118
+
119
+ # Step 1: Get the allocated variant
120
+ variant = client.get_variant("rag-prompt-experiment", user_context, input_variables)
121
+ print(f"Variant: {variant.variant_key}, Model: {variant.model}")
122
+
123
+ # Step 2: Call your LLM with the variant's prompt template
124
+ prompt = variant.prompt_template.format(**input_variables)
125
+ start = time.time()
126
+ llm_response = call_your_llm(prompt, model=variant.model) # your LLM call
127
+ latency = int((time.time() - start) * 1000)
128
+
129
+ # Step 3: Submit the response for 41-dimensional evaluation
130
+ result = client.submit_response(
131
+ experiment_key="rag-prompt-experiment",
132
+ variant_key=variant.variant_key,
133
+ executed_prompt=prompt,
134
+ response=llm_response,
135
+ user_context=user_context,
136
+ input_variables=input_variables,
137
+ provider=variant.provider,
138
+ model=variant.model,
139
+ latency_ms=latency,
140
+ )
141
+ print(f"Submitted: {result.status}")
142
+ ```
143
+
144
+ ### Managed Execution
145
+
146
+ Variably selects the variant, calls the LLM, and evaluates — all in one call.
147
+
148
+ ```python
149
+ response = client.evaluate_prompt(
150
+ experiment_key="rag-prompt-experiment",
151
+ user_context={"user_id": "user-123"},
152
+ input_variables={"query": "What are the symptoms of Type 2 diabetes?"},
153
+ evaluation_mode="full", # "full" | "fast"
154
+ )
155
+
156
+ print(f"Content: {response.content}")
157
+ print(f"Model: {response.model}, Latency: {response.latency_ms}ms")
158
+ print(f"Tokens: {response.token_usage}")
159
+ print(f"Quality Score: {response.quality_score}")
160
+ ```
161
+
162
+ ## Configuration
163
+
164
+ ```python
165
+ from variably import VariablyConfig, VariablyClient
166
+
167
+ config = VariablyConfig(
168
+ api_key="your-api-key",
169
+ base_url="https://api.variably.com", # default: http://localhost:8080
170
+ environment="production", # default: development
171
+ timeout=5000, # timeout in milliseconds, default: 5000
172
+ retry_attempts=3, # default: 3
173
+ enable_analytics=True, # default: True
174
+ cache={
175
+ "ttl": 300, # TTL in seconds, default: 300 (5 minutes)
176
+ "max_size": 1000, # default: 1000
177
+ "enabled": True # default: True
178
+ },
179
+ log_level="INFO" # DEBUG, INFO, WARNING, ERROR
180
+ )
181
+
182
+ client = VariablyClient(config)
183
+ ```
184
+
185
+ ## Advanced Usage
186
+
187
+ ### Environment Variables
188
+
189
+ You can create a client using environment variables:
190
+
191
+ ```python
192
+ from variably import create_client_from_env
193
+
194
+ # Uses these environment variables:
195
+ # VARIABLY_API_KEY (required)
196
+ # VARIABLY_BASE_URL
197
+ # VARIABLY_ENVIRONMENT
198
+ # VARIABLY_TIMEOUT
199
+ # VARIABLY_RETRY_ATTEMPTS
200
+ # VARIABLY_ENABLE_ANALYTICS
201
+ # VARIABLY_LOG_LEVEL
202
+
203
+ client = create_client_from_env()
204
+ ```
205
+
206
+ ### Different Flag Types
207
+
208
+ ```python
209
+ # Boolean flags
210
+ bool_value = client.evaluate_flag_bool("feature-enabled", False, user_context)
211
+
212
+ # String flags
213
+ string_value = client.evaluate_flag_string("theme", "light", user_context)
214
+
215
+ # Number flags
216
+ number_value = client.evaluate_flag_number("max-items", 10, user_context)
217
+
218
+ # JSON flags
219
+ json_value = client.evaluate_flag_json("config", {"timeout": 5000}, user_context)
220
+
221
+ # Get full evaluation details
222
+ result = client.evaluate_flag("feature-flag", "default", user_context)
223
+ print(f"Value: {result.value}, Reason: {result.reason}, Cache Hit: {result.cache_hit}")
224
+ ```
225
+
226
+ ### Batch Evaluation
227
+
228
+ ```python
229
+ flags = client.evaluate_flags([
230
+ "feature-a",
231
+ "feature-b",
232
+ "feature-c"
233
+ ], user_context)
234
+
235
+ print(flags["feature-a"].value)
236
+ ```
237
+
238
+ ### Event Tracking
239
+
240
+ ```python
241
+ from datetime import datetime
242
+
243
+ # Single event
244
+ client.track({
245
+ "name": "purchase_completed",
246
+ "user_id": "user-123",
247
+ "properties": {
248
+ "amount": 99.99,
249
+ "currency": "USD",
250
+ "items": ["item-1", "item-2"]
251
+ },
252
+ "timestamp": datetime.utcnow() # optional, auto-generated if not provided
253
+ })
254
+
255
+ # Batch events
256
+ client.track_batch([
257
+ {"name": "page_view", "user_id": "user-123", "properties": {"page": "/home"}},
258
+ {"name": "button_click", "user_id": "user-123", "properties": {"button": "cta"}}
259
+ ])
260
+ ```
261
+
262
+ ### Cache Management
263
+
264
+ ```python
265
+ # Clear cache
266
+ client.clear_cache()
267
+
268
+ # Get cache stats
269
+ stats = client.cache.get_stats()
270
+ print(stats) # {"size": 10, "max_size": 1000, "enabled": True, "ttl": 300}
271
+ ```
272
+
273
+ ### Metrics
274
+
275
+ ```python
276
+ # Get SDK metrics
277
+ metrics = client.get_metrics()
278
+ print(metrics)
279
+ # {
280
+ # "api_calls": 25,
281
+ # "cache_hits": 15,
282
+ # "cache_misses": 10,
283
+ # "errors": 1,
284
+ # "average_latency": 45.2,
285
+ # "cache_hit_rate": 0.6,
286
+ # "error_rate": 0.04,
287
+ # "flags_evaluated": 20,
288
+ # "gates_evaluated": 5,
289
+ # "events_tracked": 12,
290
+ # "start_time": "2023-10-01T12:00:00Z",
291
+ # "uptime_seconds": 3600
292
+ # }
293
+ ```
294
+
295
+ ### Context Manager
296
+
297
+ ```python
298
+ # Use with context manager for automatic cleanup
299
+ with VariablyClient({"api_key": "your-api-key"}) as client:
300
+ result = client.evaluate_flag_bool("feature", False, user_context)
301
+ # client.close() is called automatically
302
+ ```
303
+
304
+ ### Custom Logger
305
+
306
+ ```python
307
+ from variably import VariablyClient, create_logger
308
+
309
+ # Create custom logger
310
+ logger = create_logger(
311
+ name="my-app",
312
+ level="DEBUG",
313
+ structured=True, # JSON logging
314
+ silent=False
315
+ )
316
+
317
+ # Client will use the custom logger
318
+ client = VariablyClient({
319
+ "api_key": "your-api-key",
320
+ "log_level": "DEBUG"
321
+ })
322
+ ```
323
+
324
+ ## Error Handling
325
+
326
+ ```python
327
+ from variably import (
328
+ VariablyError,
329
+ NetworkError,
330
+ AuthenticationError,
331
+ ValidationError,
332
+ RateLimitError,
333
+ TimeoutError,
334
+ ConfigurationError
335
+ )
336
+
337
+ try:
338
+ result = client.evaluate_flag("my-flag", False, user_context)
339
+ except AuthenticationError:
340
+ print("Invalid API key")
341
+ except NetworkError as e:
342
+ print(f"Network error: {e.status_code}")
343
+ except ValidationError as e:
344
+ print(f"Validation error in field: {e.field}")
345
+ except RateLimitError as e:
346
+ print(f"Rate limited, retry after {e.retry_after} seconds")
347
+ except TimeoutError:
348
+ print("Request timed out")
349
+ except ConfigurationError as e:
350
+ print(f"Configuration error in parameter: {e.parameter}")
351
+ except VariablyError as e:
352
+ print(f"Variably SDK error: {e}")
353
+ ```
354
+
355
+ ## Type Hints
356
+
357
+ The SDK includes full type hints for better IDE support:
358
+
359
+ ```python
360
+ from typing import Dict, Any
361
+ from variably import VariablyClient, UserContext, FlagResult
362
+
363
+ user_context: UserContext = {
364
+ "user_id": "user-123",
365
+ "email": "user@example.com",
366
+ "attributes": {
367
+ "plan": "premium",
368
+ "signup_date": "2023-01-01"
369
+ }
370
+ }
371
+
372
+ result: FlagResult = client.evaluate_flag("feature", False, user_context)
373
+ ```
374
+
375
+ ## Async Support
376
+
377
+ For async applications, you can wrap the synchronous client:
378
+
379
+ ```python
380
+ import asyncio
381
+ from concurrent.futures import ThreadPoolExecutor
382
+ from variably import VariablyClient
383
+
384
+ class AsyncVariablyClient:
385
+ def __init__(self, config):
386
+ self.client = VariablyClient(config)
387
+ self.executor = ThreadPoolExecutor(max_workers=4)
388
+
389
+ async def evaluate_flag_bool(self, flag_key, default_value, user_context):
390
+ loop = asyncio.get_event_loop()
391
+ return await loop.run_in_executor(
392
+ self.executor,
393
+ self.client.evaluate_flag_bool,
394
+ flag_key, default_value, user_context
395
+ )
396
+
397
+ async def close(self):
398
+ self.client.close()
399
+ self.executor.shutdown(wait=True)
400
+
401
+ # Usage
402
+ async def main():
403
+ client = AsyncVariablyClient({"api_key": "your-api-key"})
404
+
405
+ result = await client.evaluate_flag_bool("feature", False, {
406
+ "user_id": "user-123"
407
+ })
408
+
409
+ await client.close()
410
+
411
+ asyncio.run(main())
412
+ ```
413
+
414
+ ## Development
415
+
416
+ ### Setup
417
+
418
+ ```bash
419
+ # Install development dependencies
420
+ pip install -e ".[dev]"
421
+ ```
422
+
423
+ ### Testing
424
+
425
+ ```bash
426
+ pytest
427
+ ```
428
+
429
+ ### Code Quality
430
+
431
+ ```bash
432
+ # Format code
433
+ black src/ tests/
434
+
435
+ # Sort imports
436
+ isort src/ tests/
437
+
438
+ # Lint
439
+ flake8 src/ tests/
440
+
441
+ # Type check
442
+ mypy src/
443
+ ```
444
+
445
+ ## Publishing to PyPI
446
+
447
+ ### Prerequisites
448
+
449
+ 1. Create a PyPI account at https://pypi.org/account/register/
450
+ 2. Generate an API token at https://pypi.org/manage/account/token/
451
+ - Scope: select "Entire account" for first upload, or project-specific after that
452
+ 3. Install build tools:
453
+ ```bash
454
+ pip install build twine
455
+ ```
456
+
457
+ ### Configure PyPI credentials
458
+
459
+ Create `~/.pypirc`:
460
+
461
+ ```ini
462
+ [distutils]
463
+ index-servers = pypi
464
+
465
+ [pypi]
466
+ username = __token__
467
+ password = pypi-YOUR_API_TOKEN_HERE
468
+ ```
469
+
470
+ Secure the file:
471
+
472
+ ```bash
473
+ chmod 600 ~/.pypirc
474
+ ```
475
+
476
+ ### Build and publish
477
+
478
+ ```bash
479
+ # 1. Clean previous builds
480
+ rm -rf dist/ build/ src/*.egg-info
481
+
482
+ # 2. Build sdist and wheel
483
+ python -m build
484
+
485
+ # 3. Verify the package (optional but recommended)
486
+ twine check dist/*
487
+
488
+ # 4. Upload to TestPyPI first (optional, for dry-run)
489
+ twine upload --repository testpypi dist/*
490
+
491
+ # 5. Upload to PyPI
492
+ twine upload dist/*
493
+ ```
494
+
495
+ ### Verify the published package
496
+
497
+ ```bash
498
+ pip install variably-sdk==2.0.0
499
+ python -c "from variably import VariablyClient, PromptVariant; print('OK')"
500
+ ```
501
+
502
+ ### Version bumping checklist
503
+
504
+ When releasing a new version, update these files:
505
+ - `src/variably/version.py` — `__version__`
506
+ - `pyproject.toml` — `version`
507
+ - `src/variably/http_client.py` — `User-Agent` header
508
+
509
+ ## Requirements
510
+
511
+ - Python 3.7+
512
+ - requests >= 2.25.0
513
+
514
+ ## License
515
+
516
+ MIT License - see LICENSE file for details.