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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pytest:*)",
5
+ "Bash(ruff check:*)",
6
+ "Bash(ruff format:*)",
7
+ "Bash(mypy src:*)"
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,17 @@
1
+ # Build output
2
+ dist/
3
+
4
+ # Test coverage
5
+ .coverage
6
+
7
+ # Tool caches
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .pytest_cache/
11
+
12
+ # Local Python version pin
13
+ .python-version
14
+
15
+ # Byte-compiled files
16
+ __pycache__/
17
+ *.pyc
@@ -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)