upgrade-client-lib 6.5.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.
- upgrade_client_lib-6.5.0/.claude/settings.local.json +10 -0
- upgrade_client_lib-6.5.0/.gitignore +17 -0
- upgrade_client_lib-6.5.0/BUILD_PLAN.md +349 -0
- upgrade_client_lib-6.5.0/CLAUDE.md +7 -0
- upgrade_client_lib-6.5.0/Makefile +25 -0
- upgrade_client_lib-6.5.0/PKG-INFO +214 -0
- upgrade_client_lib-6.5.0/README.md +184 -0
- upgrade_client_lib-6.5.0/pyproject.toml +85 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/__init__.py +6 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/api_service.py +339 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/assignment.py +190 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/client.py +347 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/data_service.py +107 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/exceptions.py +14 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/types/__init__.py +80 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/types/enums.py +25 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/types/requests.py +92 -0
- upgrade_client_lib-6.5.0/src/upgrade_client_lib/types/responses.py +102 -0
- upgrade_client_lib-6.5.0/tests/__init__.py +0 -0
- upgrade_client_lib-6.5.0/tests/test_api_service.py +480 -0
- upgrade_client_lib-6.5.0/tests/test_assignment.py +351 -0
- upgrade_client_lib-6.5.0/tests/test_client.py +575 -0
- upgrade_client_lib-6.5.0/tests/test_data_service.py +319 -0
- upgrade_client_lib-6.5.0/tests/test_placeholder.py +7 -0
- upgrade_client_lib-6.5.0/tests/test_types.py +348 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# UpGrade Python Client Library — Build Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Produce a publishable, installable Python package on PyPI that provides full feature parity with the existing JS and Java UpGrade client libraries. The library will target the UpGrade v6 client API.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Phase 1: Project Scaffolding and Infrastructure
|
|
10
|
+
|
|
11
|
+
Set up the Python package structure, tooling, and CI foundation before writing any library code.
|
|
12
|
+
|
|
13
|
+
### Tasks
|
|
14
|
+
|
|
15
|
+
- Initialize package layout using `src/` layout convention:
|
|
16
|
+
```
|
|
17
|
+
clientlibs/python/
|
|
18
|
+
├── src/
|
|
19
|
+
│ └── upgrade_client_lib/
|
|
20
|
+
│ ├── __init__.py
|
|
21
|
+
│ ├── client.py
|
|
22
|
+
│ ├── assignment.py
|
|
23
|
+
│ ├── api_service.py
|
|
24
|
+
│ ├── data_service.py
|
|
25
|
+
│ ├── types/
|
|
26
|
+
│ │ ├── __init__.py
|
|
27
|
+
│ │ ├── enums.py
|
|
28
|
+
│ │ ├── requests.py
|
|
29
|
+
│ │ └── responses.py
|
|
30
|
+
│ └── exceptions.py
|
|
31
|
+
├── tests/
|
|
32
|
+
├── pyproject.toml
|
|
33
|
+
├── README.md
|
|
34
|
+
└── .python-version
|
|
35
|
+
```
|
|
36
|
+
- Configure `pyproject.toml` with:
|
|
37
|
+
- Package metadata (name, version, description, authors, license, classifiers)
|
|
38
|
+
- Python `>=3.9` minimum version requirement
|
|
39
|
+
- Runtime dependencies: `httpx` (async-capable HTTP), `pydantic` (request/response models)
|
|
40
|
+
- Dev dependencies: `pytest`, `pytest-asyncio`, `respx` (httpx mock), `ruff`, `mypy`
|
|
41
|
+
- Build backend: `hatchling` or `flit`
|
|
42
|
+
- Configure `ruff` for linting and formatting
|
|
43
|
+
- Configure `mypy` for strict type checking
|
|
44
|
+
- Set up `pytest` with `pytest-asyncio` for async test support
|
|
45
|
+
- Add a `Makefile` or `justfile` with targets: `lint`, `test`, `build`, `publish`
|
|
46
|
+
|
|
47
|
+
### Deliverable
|
|
48
|
+
|
|
49
|
+
An installable (but empty) package skeleton that passes linting and a placeholder test.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Phase 2: Types, Models, and Enums
|
|
54
|
+
|
|
55
|
+
Define all data structures used across request/response flows. Mirrors the types layer in the JS library (`src/types/`) and request/response beans in Java.
|
|
56
|
+
|
|
57
|
+
### Enums
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
class MarkedDecisionPointStatus(str, Enum):
|
|
61
|
+
CONDITION_APPLIED = "condition applied"
|
|
62
|
+
CONDITION_FAILED_TO_APPLY = "condition not applied"
|
|
63
|
+
NO_CONDITION_ASSIGNED = "no condition assigned"
|
|
64
|
+
|
|
65
|
+
class ExperimentType(str, Enum):
|
|
66
|
+
SIMPLE = "Simple"
|
|
67
|
+
FACTORIAL = "Factorial"
|
|
68
|
+
|
|
69
|
+
class PayloadType(str, Enum):
|
|
70
|
+
STRING = "string"
|
|
71
|
+
JSON = "json"
|
|
72
|
+
CSV = "csv"
|
|
73
|
+
|
|
74
|
+
class BinaryRewardValue(str, Enum):
|
|
75
|
+
SUCCESS = "SUCCESS"
|
|
76
|
+
FAILURE = "FAILURE"
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Request Models (Pydantic)
|
|
81
|
+
|
|
82
|
+
- `InitializeUserRequest` — userId, group, workingGroup
|
|
83
|
+
- `SetGroupMembershipRequest` — id, group
|
|
84
|
+
- `SetWorkingGroupRequest` — id, workingGroup
|
|
85
|
+
- `AssignRequest` — userId, context
|
|
86
|
+
- `MarkDecisionPointRequest` — userId, site, target, status, condition, uniquifier, clientError
|
|
87
|
+
- `LogRequest` — userId, value (list of `LogInput`)
|
|
88
|
+
- `LogInput` — userId, timestamp, metrics (`LogMetrics`)
|
|
89
|
+
- `LogMetrics` — attributes, groupedMetrics (list of `LogGroupMetrics`)
|
|
90
|
+
- `LogGroupMetrics` — groupClass, groupKey, groupUniquifier, attributes
|
|
91
|
+
- `UserAliasRequest` — userId, aliases
|
|
92
|
+
- `FeatureFlagRequest` — userId, context
|
|
93
|
+
- `SendRewardRequest` — rewardValue (`BinaryRewardValue`), experimentId (optional), context (optional), decisionPoint (optional: site + target)
|
|
94
|
+
|
|
95
|
+
### Response Models (Pydantic)
|
|
96
|
+
|
|
97
|
+
- `InitializeUserResponse` — id, group, workingGroup
|
|
98
|
+
- `ExperimentAssignment` — site, target, experimentType, assignedCondition, assignedFactor
|
|
99
|
+
- `Condition` — id, conditionCode, payload
|
|
100
|
+
- `Factor` — level, payload
|
|
101
|
+
- `Payload` — type, value
|
|
102
|
+
- `FeatureFlag` — key, status, variationValue
|
|
103
|
+
- `MarkDecisionPointResponse` — userId, site, target, experimentId
|
|
104
|
+
- `LogEventResponse` — id, uniquifier, timeStamp, data
|
|
105
|
+
- `UserAliasResponse` — userId, aliases
|
|
106
|
+
- `SendRewardResponse` — message, request (rewardValue, experimentId, context, decisionPoint), reward (variable, value, mooclet, version, learner)
|
|
107
|
+
- `ErrorResponse` — message, httpStatusCode, type
|
|
108
|
+
|
|
109
|
+
### Deliverable
|
|
110
|
+
|
|
111
|
+
All models defined, type-checked by mypy, and unit tested with valid/invalid fixture data.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Phase 3: HTTP Layer
|
|
116
|
+
|
|
117
|
+
Implement the HTTP communication layer that wraps `httpx`. Both sync and async interfaces are provided.
|
|
118
|
+
|
|
119
|
+
### `DefaultHttpClient`
|
|
120
|
+
|
|
121
|
+
- Wraps `httpx.Client` (sync) and `httpx.AsyncClient` (async)
|
|
122
|
+
- Sends `Content-Type: application/json` and `Authorization: Bearer <token>` headers
|
|
123
|
+
- Accepts a `clientSessionId` header on every request
|
|
124
|
+
- Raises `UpgradeApiError` (a library-defined exception) on non-2xx responses
|
|
125
|
+
|
|
126
|
+
### `ApiService`
|
|
127
|
+
|
|
128
|
+
Maps library operations to HTTP endpoints (v6):
|
|
129
|
+
|
|
130
|
+
| Method | HTTP | Endpoint |
|
|
131
|
+
|--------|------|----------|
|
|
132
|
+
| `init_user` | POST | `/api/v6/init` |
|
|
133
|
+
| `set_group_membership` | PATCH | `/api/v6/groupmembership` |
|
|
134
|
+
| `set_working_group` | PATCH | `/api/v6/workinggroup` |
|
|
135
|
+
| `get_all_experiment_conditions` | POST | `/api/v6/assign` |
|
|
136
|
+
| `mark_decision_point` | POST | `/api/v6/mark` |
|
|
137
|
+
| `get_all_feature_flags` | POST | `/api/v6/featureflag` |
|
|
138
|
+
| `log` | POST | `/api/v6/log` |
|
|
139
|
+
| `set_alt_user_ids` | PATCH | `/api/v6/useraliases` |
|
|
140
|
+
| `send_reward` | POST | `/api/v6/reward` |
|
|
141
|
+
|
|
142
|
+
Both sync and async variants for each method.
|
|
143
|
+
|
|
144
|
+
### Deliverable
|
|
145
|
+
|
|
146
|
+
HTTP layer fully tested with `respx` (httpx mock). All endpoints covered.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Phase 4: Caching Layer
|
|
151
|
+
|
|
152
|
+
Implement `DataService` to cache experiment assignments and feature flags in memory, matching the behavior of the JS `DataService`.
|
|
153
|
+
|
|
154
|
+
- Store assignments keyed by `"{site}|{target}"`
|
|
155
|
+
- Store feature flags keyed by flag key
|
|
156
|
+
- Support `ignore_cache=True` bypass on fetch methods (matches Java's `ignoreCache` parameter)
|
|
157
|
+
- Cache is per-client-instance (not global)
|
|
158
|
+
- Cache is invalidated by re-calling `init()`
|
|
159
|
+
|
|
160
|
+
### Deliverable
|
|
161
|
+
|
|
162
|
+
`DataService` unit tested with cache-hit, cache-miss, and cache-bypass scenarios.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Phase 5: Assignment Class
|
|
167
|
+
|
|
168
|
+
Implement the `Assignment` wrapper that provides a high-level interface over a raw `ExperimentAssignment` response, matching both JS and Java `Assignment` classes.
|
|
169
|
+
|
|
170
|
+
### Public Interface
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
class Assignment:
|
|
174
|
+
def get_condition(self) -> str: ...
|
|
175
|
+
def get_payload(self) -> Payload | None: ...
|
|
176
|
+
def get_experiment_type(self) -> ExperimentType: ...
|
|
177
|
+
def get_factor_level(self, factor: str) -> str: ... # Factorial only
|
|
178
|
+
def get_factor_payload(self, factor: str) -> Payload | None: ... # Factorial only
|
|
179
|
+
@property
|
|
180
|
+
def factors(self) -> list[str]: ... # Factorial only
|
|
181
|
+
|
|
182
|
+
# Convenience mark method (delegates to UpgradeClient)
|
|
183
|
+
async def mark_decision_point(
|
|
184
|
+
self,
|
|
185
|
+
status: MarkedDecisionPointStatus,
|
|
186
|
+
uniquifier: str = "",
|
|
187
|
+
client_error: str = "",
|
|
188
|
+
) -> MarkDecisionPointResponse: ...
|
|
189
|
+
|
|
190
|
+
# Sync variant
|
|
191
|
+
def mark_decision_point_sync(...) -> MarkDecisionPointResponse: ...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Deliverable
|
|
195
|
+
|
|
196
|
+
`Assignment` unit tested for simple and factorial experiment types.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Phase 6: Main Client (`UpgradeClient`)
|
|
201
|
+
|
|
202
|
+
Implement the primary public API class. Exposes both async and sync interfaces.
|
|
203
|
+
|
|
204
|
+
### Constructor
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
class UpgradeClient:
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
user_id: str,
|
|
211
|
+
host_url: str,
|
|
212
|
+
context: str,
|
|
213
|
+
token: str = "",
|
|
214
|
+
client_session_id: str | None = None, # auto-generated UUID if None
|
|
215
|
+
): ...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Public Methods (Async)
|
|
219
|
+
|
|
220
|
+
| Method | Signature |
|
|
221
|
+
|--------|-----------|
|
|
222
|
+
| `init` | `async init(group=None, working_group=None) -> InitializeUserResponse` |
|
|
223
|
+
| `set_group_membership` | `async set_group_membership(group: dict) -> InitializeUserResponse` |
|
|
224
|
+
| `set_working_group` | `async set_working_group(working_group: dict) -> InitializeUserResponse` |
|
|
225
|
+
| `get_all_experiment_conditions` | `async get_all_experiment_conditions(ignore_cache=False) -> list[Assignment]` |
|
|
226
|
+
| `get_decision_point_assignment` | `async get_decision_point_assignment(site, target="") -> Assignment \| None` |
|
|
227
|
+
| `mark_decision_point` | `async mark_decision_point(site, target, condition, status, uniquifier="", client_error="") -> MarkDecisionPointResponse` |
|
|
228
|
+
| `get_all_feature_flags` | `async get_all_feature_flags(ignore_cache=False) -> list[FeatureFlag]` |
|
|
229
|
+
| `has_feature_flag` | `async has_feature_flag(key: str) -> bool` |
|
|
230
|
+
| `log` | `async log(metrics: list[LogInput]) -> list[LogEventResponse]` |
|
|
231
|
+
| `set_alt_user_ids` | `async set_alt_user_ids(aliases: list[str]) -> UserAliasResponse` |
|
|
232
|
+
| `send_reward` | `async send_reward(reward_value: BinaryRewardValue, experiment_id=None, context=None, decision_point=None) -> SendRewardResponse` |
|
|
233
|
+
|
|
234
|
+
### Sync Variants
|
|
235
|
+
|
|
236
|
+
Wrap each async method with a sync equivalent using `asyncio.run()` or a shared event loop helper, for use in non-async applications.
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
client.init_sync(...)
|
|
240
|
+
client.get_decision_point_assignment_sync(...)
|
|
241
|
+
# etc.
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Static Constants
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
UpgradeClient.MARKED_DECISION_POINT_STATUS = MarkedDecisionPointStatus
|
|
248
|
+
UpgradeClient.BINARY_REWARD_VALUE = BinaryRewardValue
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Deliverable
|
|
252
|
+
|
|
253
|
+
Full client tested with integration-style tests using mocked HTTP responses. All methods covered.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Phase 7: Packaging and Distribution
|
|
258
|
+
|
|
259
|
+
Prepare the library for publication to PyPI.
|
|
260
|
+
|
|
261
|
+
### Tasks
|
|
262
|
+
|
|
263
|
+
- Finalize `pyproject.toml`:
|
|
264
|
+
- Name: `upgrade-client-lib`
|
|
265
|
+
- Version: `6.5.0` (matching JS library version for parity signaling)
|
|
266
|
+
- Homepage and repository links to UpGrade GitHub
|
|
267
|
+
- Trove classifiers (Python versions, license, topic)
|
|
268
|
+
- Write `README.md` with:
|
|
269
|
+
- Installation instructions (`pip install upgrade-client-lib`)
|
|
270
|
+
- Quick-start example (async and sync)
|
|
271
|
+
- Full API reference summary
|
|
272
|
+
- Link to UpGrade documentation
|
|
273
|
+
- Verify `python -m build` produces a clean sdist and wheel
|
|
274
|
+
- Test install from wheel in a clean virtualenv
|
|
275
|
+
- Configure PyPI publishing via Trusted Publisher (OIDC) in GitHub Actions
|
|
276
|
+
- Add CI workflow (`.github/workflows/python-publish.yml`) that publishes on tagged release
|
|
277
|
+
|
|
278
|
+
### Deliverable
|
|
279
|
+
|
|
280
|
+
Package published to TestPyPI. CI workflow validated end-to-end.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Phase 8: Testing and Quality
|
|
285
|
+
|
|
286
|
+
Ensure the library is production-ready with comprehensive test and quality coverage.
|
|
287
|
+
|
|
288
|
+
### Test Coverage Targets
|
|
289
|
+
|
|
290
|
+
- Unit tests: all public methods, all model validations, all enum values
|
|
291
|
+
- Integration tests: full request/response cycle mocked at HTTP layer
|
|
292
|
+
- Edge cases: cache behavior, `None` returns for missing assignments, error responses, Factorial vs Simple experiment handling
|
|
293
|
+
|
|
294
|
+
### Quality Gates
|
|
295
|
+
|
|
296
|
+
- `mypy --strict` passes with zero errors
|
|
297
|
+
- `ruff` reports zero lint violations
|
|
298
|
+
- `pytest` coverage ≥ 90%
|
|
299
|
+
- All tests pass on Python 3.9, 3.10, 3.11, 3.12 (matrix CI)
|
|
300
|
+
|
|
301
|
+
### Deliverable
|
|
302
|
+
|
|
303
|
+
CI pipeline green across all Python versions. Coverage report published as CI artifact.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Phase 9: Documentation and Release
|
|
308
|
+
|
|
309
|
+
- Finalize `CHANGELOG.md`
|
|
310
|
+
- Tag `v6.5.0` release in git
|
|
311
|
+
- Publish to PyPI production
|
|
312
|
+
- Add Python library entry to the root UpGrade repository `README.md` alongside JS and Java entries
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Dependency Decisions
|
|
317
|
+
|
|
318
|
+
| Concern | Choice | Rationale |
|
|
319
|
+
|---------|--------|-----------|
|
|
320
|
+
| HTTP | `httpx` | Native async/sync support; mirrors axios (JS) ergonomics |
|
|
321
|
+
| Data models | `pydantic v2` | Runtime validation, serialization, IDE-friendly types |
|
|
322
|
+
| Async test support | `pytest-asyncio` | Standard async pytest integration |
|
|
323
|
+
| HTTP mocking | `respx` | First-class httpx mock library |
|
|
324
|
+
| Linting/formatting | `ruff` | Fast, single-tool replacement for flake8 + isort + black |
|
|
325
|
+
| Type checking | `mypy` | Standard Python static type checker |
|
|
326
|
+
| Build backend | `hatchling` | Modern, PEP 517/518 compliant; simple configuration |
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Feature Parity Matrix
|
|
331
|
+
|
|
332
|
+
| Feature | JS | Java | Python |
|
|
333
|
+
|---------|----|------|--------|
|
|
334
|
+
| User initialization | ✓ | ✓ | Phase 6 |
|
|
335
|
+
| Group membership | ✓ | ✓ | Phase 6 |
|
|
336
|
+
| Working group | ✓ | ✓ | Phase 6 |
|
|
337
|
+
| Experiment assignments (cached) | ✓ | ✓ | Phase 6 |
|
|
338
|
+
| Decision point marking | ✓ | ✓ | Phase 6 |
|
|
339
|
+
| Assignment wrapper class | ✓ | ✓ | Phase 5 |
|
|
340
|
+
| Factorial experiment support | ✓ | ✓ | Phase 5 |
|
|
341
|
+
| Payload support | ✓ | ✓ | Phase 5 |
|
|
342
|
+
| Feature flags (cached) | ✓ | ✓ | Phase 6 |
|
|
343
|
+
| Metrics logging (grouped) | ✓ | ✓ | Phase 6 |
|
|
344
|
+
| Alternate user IDs | ✓ | ✓ | Phase 6 |
|
|
345
|
+
| Async interface | ✓ | — | Phase 6 |
|
|
346
|
+
| Sync interface | — | ✓ | Phase 6 |
|
|
347
|
+
| Cache bypass (`ignore_cache`) | — | ✓ | Phase 4 |
|
|
348
|
+
| Adaptive experiment rewards | ✓ | — | Phase 6 |
|
|
349
|
+
| Session-only feature flag groups | ✓ | — | Post-v1 |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# UpGrade Python Client Library
|
|
2
|
+
|
|
3
|
+
This directory contains a Python client library for applications that use the UpGrade client API.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The library provides a Python interface for interacting with the UpGrade experimentation platform's client API, allowing Python-based applications to integrate with UpGrade for feature flagging and A/B testing.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.PHONY: install lint typecheck test build publish clean
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
pip install -e ".[dev]"
|
|
5
|
+
|
|
6
|
+
lint:
|
|
7
|
+
ruff check src tests
|
|
8
|
+
ruff format --check src tests
|
|
9
|
+
|
|
10
|
+
typecheck:
|
|
11
|
+
mypy src
|
|
12
|
+
|
|
13
|
+
test:
|
|
14
|
+
pytest
|
|
15
|
+
|
|
16
|
+
build:
|
|
17
|
+
python -m build
|
|
18
|
+
|
|
19
|
+
publish:
|
|
20
|
+
twine upload dist/*
|
|
21
|
+
|
|
22
|
+
clean:
|
|
23
|
+
rm -rf dist build .mypy_cache .ruff_cache .pytest_cache htmlcov
|
|
24
|
+
find . -type d -name "__pycache__" -exec rm -rf {} +
|
|
25
|
+
find . -type d -name "*.egg-info" -exec rm -rf {} +
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: upgrade-client-lib
|
|
3
|
+
Version: 6.5.0
|
|
4
|
+
Summary: Python client library for the UpGrade experimentation platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/CarnegieLearningWeb/UpGrade
|
|
6
|
+
Project-URL: Repository, https://github.com/CarnegieLearningWeb/UpGrade
|
|
7
|
+
Project-URL: Issues, https://github.com/CarnegieLearningWeb/UpGrade/issues
|
|
8
|
+
Author-email: Carnegie Learning <upgrade@carnegielearning.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# upgrade-client-lib
|
|
32
|
+
|
|
33
|
+
Python client library for the [UpGrade](https://github.com/CarnegieLearningWeb/UpGrade) experimentation platform.
|
|
34
|
+
|
|
35
|
+
Supports Python 3.9+ with both async and synchronous interfaces.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install upgrade-client-lib
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Async
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from upgrade_client_lib import UpgradeClient
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
client = UpgradeClient(
|
|
53
|
+
user_id="user-123",
|
|
54
|
+
host_url="https://your-upgrade-instance.com",
|
|
55
|
+
context="my-app",
|
|
56
|
+
token="your-token", # optional
|
|
57
|
+
client_session_id="sess-1", # optional, auto-generated if omitted
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
await client.init()
|
|
61
|
+
|
|
62
|
+
# Experiment assignments
|
|
63
|
+
assignment = await client.get_decision_point_assignment("home", "banner")
|
|
64
|
+
if assignment:
|
|
65
|
+
condition = assignment.get_condition() # e.g. "treatment"
|
|
66
|
+
payload = assignment.get_payload() # Payload | None
|
|
67
|
+
await assignment.mark_decision_point(
|
|
68
|
+
UpgradeClient.MARKED_DECISION_POINT_STATUS.CONDITION_APPLIED
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Feature flags
|
|
72
|
+
if await client.has_feature_flag("new-dashboard"):
|
|
73
|
+
pass # enable feature
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Sync
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from upgrade_client_lib import UpgradeClient
|
|
82
|
+
|
|
83
|
+
client = UpgradeClient("user-123", "https://your-upgrade-instance.com", "my-app")
|
|
84
|
+
client.init_sync()
|
|
85
|
+
|
|
86
|
+
assignment = client.get_decision_point_assignment_sync("home", "banner")
|
|
87
|
+
if assignment:
|
|
88
|
+
condition = assignment.get_condition()
|
|
89
|
+
assignment.mark_decision_point_sync(
|
|
90
|
+
UpgradeClient.MARKED_DECISION_POINT_STATUS.CONDITION_APPLIED
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### `UpgradeClient(user_id, host_url, context, token="", client_session_id=None)`
|
|
97
|
+
|
|
98
|
+
| Parameter | Type | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `user_id` | `str` | Unique identifier for the user |
|
|
101
|
+
| `host_url` | `str` | Base URL of the UpGrade API server |
|
|
102
|
+
| `context` | `str` | Application context string scoping experiment/flag lookups |
|
|
103
|
+
| `token` | `str` | Optional bearer token for the `Authorization` header |
|
|
104
|
+
| `client_session_id` | `str \| None` | Optional session ID; auto-generated UUID when omitted |
|
|
105
|
+
|
|
106
|
+
Every method has an async variant (no suffix) and a synchronous variant (`_sync` suffix).
|
|
107
|
+
|
|
108
|
+
### Initialization
|
|
109
|
+
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `init(group=None, working_group=None)` | Initialize the user and clear the local cache |
|
|
113
|
+
| `set_group_membership(group)` | Update the user's group membership |
|
|
114
|
+
| `set_working_group(working_group)` | Update the user's working group |
|
|
115
|
+
| `set_alt_user_ids(aliases)` | Register alternate identifiers for this user |
|
|
116
|
+
|
|
117
|
+
### Experiments
|
|
118
|
+
|
|
119
|
+
| Method | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `get_all_experiment_conditions(ignore_cache=False)` | Return all `Assignment` objects for this user/context |
|
|
122
|
+
| `get_decision_point_assignment(site, target="")` | Return the `Assignment` for a specific decision point, or `None` |
|
|
123
|
+
| `mark_decision_point(site, target, condition, status, uniquifier="", client_error="")` | Record that the user encountered a decision point |
|
|
124
|
+
|
|
125
|
+
### Feature Flags
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `get_all_feature_flags(ignore_cache=False)` | Return all active `FeatureFlag` objects |
|
|
130
|
+
| `has_feature_flag(key)` | Return `True` if the named flag is active for this user |
|
|
131
|
+
|
|
132
|
+
### Metrics & Rewards
|
|
133
|
+
|
|
134
|
+
| Method | Description |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `log(metrics)` | Send outcome metric events |
|
|
137
|
+
| `send_reward(reward_value, experiment_id=None, context=None, decision_point=None)` | Send a binary reward signal for an adaptive experiment |
|
|
138
|
+
|
|
139
|
+
`log()` accepts a list of `LogInput` objects. `LogInput`, `LogMetrics`, and `LogGroupMetrics` must be imported from `upgrade_client_lib.types`:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from upgrade_client_lib.types import LogInput, LogMetrics, LogGroupMetrics
|
|
143
|
+
|
|
144
|
+
# Simple metrics
|
|
145
|
+
log = client.log_sync([
|
|
146
|
+
LogInput(
|
|
147
|
+
timestamp="2024-01-01T00:00:00Z",
|
|
148
|
+
metrics=LogMetrics(attributes={"totalTimeSeconds": 95}),
|
|
149
|
+
)
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
# With grouped metrics
|
|
153
|
+
log = client.log_sync([
|
|
154
|
+
LogInput(
|
|
155
|
+
timestamp="2024-01-01T00:00:00Z",
|
|
156
|
+
metrics=LogMetrics(
|
|
157
|
+
attributes={"totalTimeSeconds": 95},
|
|
158
|
+
groupedMetrics=[
|
|
159
|
+
LogGroupMetrics(
|
|
160
|
+
groupClass="quiz",
|
|
161
|
+
groupKey="quiz-1",
|
|
162
|
+
groupUniquifier="attempt-1",
|
|
163
|
+
attributes={"score": 80},
|
|
164
|
+
)
|
|
165
|
+
],
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
])
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `Assignment`
|
|
172
|
+
|
|
173
|
+
Returned by `get_all_experiment_conditions` and `get_decision_point_assignment`.
|
|
174
|
+
|
|
175
|
+
| Method / Property | Description |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `get_condition()` | Assigned condition code (`""` when none) |
|
|
178
|
+
| `get_payload()` | Condition payload (`Payload \| None`) |
|
|
179
|
+
| `get_experiment_type()` | `ExperimentType.SIMPLE` or `ExperimentType.FACTORIAL` |
|
|
180
|
+
| `factors` | List of factor names (factorial experiments only) |
|
|
181
|
+
| `get_factor_level(factor)` | Assigned level for a factor (`str \| None`) |
|
|
182
|
+
| `get_factor_payload(factor)` | Payload for a factor (`Payload \| None`) |
|
|
183
|
+
| `mark_decision_point(status, uniquifier="", client_error="")` | Mark this decision point |
|
|
184
|
+
|
|
185
|
+
### Enums
|
|
186
|
+
|
|
187
|
+
Access via class attributes so no separate import is needed:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
UpgradeClient.MARKED_DECISION_POINT_STATUS.CONDITION_APPLIED
|
|
191
|
+
UpgradeClient.MARKED_DECISION_POINT_STATUS.CONDITION_FAILED_TO_APPLY
|
|
192
|
+
UpgradeClient.MARKED_DECISION_POINT_STATUS.NO_CONDITION_ASSIGNED
|
|
193
|
+
|
|
194
|
+
UpgradeClient.BINARY_REWARD_VALUE.SUCCESS
|
|
195
|
+
UpgradeClient.BINARY_REWARD_VALUE.FAILURE
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Error Handling
|
|
199
|
+
|
|
200
|
+
API errors raise `upgrade_client_lib.exceptions.UpgradeApiError`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from upgrade_client_lib.exceptions import UpgradeApiError
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
await client.init()
|
|
207
|
+
except UpgradeApiError as e:
|
|
208
|
+
print(e.status_code, e.message)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Links
|
|
212
|
+
|
|
213
|
+
- [UpGrade repository](https://github.com/CarnegieLearningWeb/UpGrade)
|
|
214
|
+
- [UpGrade documentation](https://upgradeplatform.io)
|