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.
- variably_sdk-2.0.0/PKG-INFO +516 -0
- variably_sdk-2.0.0/README.md +471 -0
- variably_sdk-2.0.0/pyproject.toml +109 -0
- variably_sdk-2.0.0/setup.cfg +4 -0
- variably_sdk-2.0.0/setup.py +23 -0
- variably_sdk-2.0.0/src/variably/__init__.py +38 -0
- variably_sdk-2.0.0/src/variably/cache.py +146 -0
- variably_sdk-2.0.0/src/variably/client.py +614 -0
- variably_sdk-2.0.0/src/variably/config.py +99 -0
- variably_sdk-2.0.0/src/variably/errors.py +61 -0
- variably_sdk-2.0.0/src/variably/http_client.py +295 -0
- variably_sdk-2.0.0/src/variably/logger.py +117 -0
- variably_sdk-2.0.0/src/variably/types.py +260 -0
- variably_sdk-2.0.0/src/variably/version.py +3 -0
- variably_sdk-2.0.0/src/variably_sdk.egg-info/PKG-INFO +516 -0
- variably_sdk-2.0.0/src/variably_sdk.egg-info/SOURCES.txt +17 -0
- variably_sdk-2.0.0/src/variably_sdk.egg-info/dependency_links.txt +1 -0
- variably_sdk-2.0.0/src/variably_sdk.egg-info/requires.txt +19 -0
- variably_sdk-2.0.0/src/variably_sdk.egg-info/top_level.txt +1 -0
|
@@ -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.
|