qbrix 0.1.3__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.
Files changed (53) hide show
  1. qbrix-0.1.3/.codegraph/.gitignore +16 -0
  2. qbrix-0.1.3/.codegraph/config.json +140 -0
  3. qbrix-0.1.3/.env.example +1 -0
  4. qbrix-0.1.3/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  5. qbrix-0.1.3/.github/workflows/ci.yml +64 -0
  6. qbrix-0.1.3/.github/workflows/publish.yml +37 -0
  7. qbrix-0.1.3/.github/workflows/release-please.yml +24 -0
  8. qbrix-0.1.3/.gitignore +34 -0
  9. qbrix-0.1.3/.release-please-manifest.json +3 -0
  10. qbrix-0.1.3/CHANGELOG.md +26 -0
  11. qbrix-0.1.3/CLAUDE.md +114 -0
  12. qbrix-0.1.3/PKG-INFO +195 -0
  13. qbrix-0.1.3/README.md +173 -0
  14. qbrix-0.1.3/asset/logo/bb_logo.svg +13 -0
  15. qbrix-0.1.3/bin/.keep +0 -0
  16. qbrix-0.1.3/examples/quickstart.py +142 -0
  17. qbrix-0.1.3/pyproject.toml +54 -0
  18. qbrix-0.1.3/qbrix/__init__.py +91 -0
  19. qbrix-0.1.3/qbrix/_base_client.py +279 -0
  20. qbrix-0.1.3/qbrix/_client.py +72 -0
  21. qbrix-0.1.3/qbrix/_config.py +16 -0
  22. qbrix-0.1.3/qbrix/_mod_client.py +22 -0
  23. qbrix-0.1.3/qbrix/_proxies.py +33 -0
  24. qbrix-0.1.3/qbrix/_util.py +25 -0
  25. qbrix-0.1.3/qbrix/_version.py +1 -0
  26. qbrix-0.1.3/qbrix/exception.py +90 -0
  27. qbrix-0.1.3/qbrix/model/__init__.py +41 -0
  28. qbrix-0.1.3/qbrix/model/agent.py +31 -0
  29. qbrix-0.1.3/qbrix/model/auth.py +13 -0
  30. qbrix-0.1.3/qbrix/model/common.py +27 -0
  31. qbrix-0.1.3/qbrix/model/experiment.py +35 -0
  32. qbrix-0.1.3/qbrix/model/gate.py +32 -0
  33. qbrix-0.1.3/qbrix/model/pool.py +35 -0
  34. qbrix-0.1.3/qbrix/py.typed +0 -0
  35. qbrix-0.1.3/qbrix/resource/__init__.py +19 -0
  36. qbrix-0.1.3/qbrix/resource/_base.py +105 -0
  37. qbrix-0.1.3/qbrix/resource/agent.py +68 -0
  38. qbrix-0.1.3/qbrix/resource/experiment.py +167 -0
  39. qbrix-0.1.3/qbrix/resource/gate.py +184 -0
  40. qbrix-0.1.3/qbrix/resource/pool.py +108 -0
  41. qbrix-0.1.3/release-please-config.json +11 -0
  42. qbrix-0.1.3/tests/__init__.py +0 -0
  43. qbrix-0.1.3/tests/conftest.py +104 -0
  44. qbrix-0.1.3/tests/test_base_client.py +252 -0
  45. qbrix-0.1.3/tests/test_client.py +82 -0
  46. qbrix-0.1.3/tests/test_config.py +40 -0
  47. qbrix-0.1.3/tests/test_exception.py +86 -0
  48. qbrix-0.1.3/tests/test_models.py +210 -0
  49. qbrix-0.1.3/tests/test_resource_agent.py +107 -0
  50. qbrix-0.1.3/tests/test_resource_experiment.py +130 -0
  51. qbrix-0.1.3/tests/test_resource_gate.py +106 -0
  52. qbrix-0.1.3/tests/test_resource_pool.py +137 -0
  53. qbrix-0.1.3/uv.lock +869 -0
@@ -0,0 +1,16 @@
1
+ # CodeGraph data files
2
+ # These are local to each machine and should not be committed
3
+
4
+ # Database
5
+ *.db
6
+ *.db-wal
7
+ *.db-shm
8
+
9
+ # Cache
10
+ cache/
11
+
12
+ # Logs
13
+ *.log
14
+
15
+ # Hook markers
16
+ .dirty
@@ -0,0 +1,140 @@
1
+ {
2
+ "version": 1,
3
+ "include": [
4
+ "**/*.ts",
5
+ "**/*.tsx",
6
+ "**/*.js",
7
+ "**/*.jsx",
8
+ "**/*.py",
9
+ "**/*.go",
10
+ "**/*.rs",
11
+ "**/*.java",
12
+ "**/*.c",
13
+ "**/*.h",
14
+ "**/*.cpp",
15
+ "**/*.hpp",
16
+ "**/*.cc",
17
+ "**/*.cxx",
18
+ "**/*.cs",
19
+ "**/*.php",
20
+ "**/*.rb",
21
+ "**/*.swift",
22
+ "**/*.kt",
23
+ "**/*.kts",
24
+ "**/*.dart",
25
+ "**/*.svelte",
26
+ "**/*.liquid",
27
+ "**/*.pas",
28
+ "**/*.dpr",
29
+ "**/*.dpk",
30
+ "**/*.lpr",
31
+ "**/*.dfm",
32
+ "**/*.fmx"
33
+ ],
34
+ "exclude": [
35
+ "**/.git/**",
36
+ "**/node_modules/**",
37
+ "**/vendor/**",
38
+ "**/Pods/**",
39
+ "**/dist/**",
40
+ "**/build/**",
41
+ "**/out/**",
42
+ "**/bin/**",
43
+ "**/obj/**",
44
+ "**/target/**",
45
+ "**/*.min.js",
46
+ "**/*.bundle.js",
47
+ "**/.next/**",
48
+ "**/.nuxt/**",
49
+ "**/.svelte-kit/**",
50
+ "**/.output/**",
51
+ "**/.turbo/**",
52
+ "**/.cache/**",
53
+ "**/.parcel-cache/**",
54
+ "**/.vite/**",
55
+ "**/.astro/**",
56
+ "**/.docusaurus/**",
57
+ "**/.gatsby/**",
58
+ "**/.webpack/**",
59
+ "**/.nx/**",
60
+ "**/.yarn/cache/**",
61
+ "**/.pnpm-store/**",
62
+ "**/storybook-static/**",
63
+ "**/.expo/**",
64
+ "**/web-build/**",
65
+ "**/ios/Pods/**",
66
+ "**/ios/build/**",
67
+ "**/android/build/**",
68
+ "**/android/.gradle/**",
69
+ "**/__pycache__/**",
70
+ "**/.venv/**",
71
+ "**/venv/**",
72
+ "**/site-packages/**",
73
+ "**/dist-packages/**",
74
+ "**/.pytest_cache/**",
75
+ "**/.mypy_cache/**",
76
+ "**/.ruff_cache/**",
77
+ "**/.tox/**",
78
+ "**/.nox/**",
79
+ "**/*.egg-info/**",
80
+ "**/.eggs/**",
81
+ "**/go/pkg/mod/**",
82
+ "**/target/debug/**",
83
+ "**/target/release/**",
84
+ "**/.gradle/**",
85
+ "**/.m2/**",
86
+ "**/generated-sources/**",
87
+ "**/.kotlin/**",
88
+ "**/.dart_tool/**",
89
+ "**/.vs/**",
90
+ "**/.nuget/**",
91
+ "**/artifacts/**",
92
+ "**/publish/**",
93
+ "**/cmake-build-*/**",
94
+ "**/CMakeFiles/**",
95
+ "**/bazel-*/**",
96
+ "**/vcpkg_installed/**",
97
+ "**/.conan/**",
98
+ "**/Debug/**",
99
+ "**/Release/**",
100
+ "**/x64/**",
101
+ "**/release/**",
102
+ "**/*.app/**",
103
+ "**/*.asar",
104
+ "**/DerivedData/**",
105
+ "**/.build/**",
106
+ "**/.swiftpm/**",
107
+ "**/xcuserdata/**",
108
+ "**/Carthage/Build/**",
109
+ "**/SourcePackages/**",
110
+ "**/__history/**",
111
+ "**/__recovery/**",
112
+ "**/*.dcu",
113
+ "**/.composer/**",
114
+ "**/storage/framework/**",
115
+ "**/bootstrap/cache/**",
116
+ "**/.bundle/**",
117
+ "**/tmp/cache/**",
118
+ "**/public/assets/**",
119
+ "**/public/packs/**",
120
+ "**/.yardoc/**",
121
+ "**/coverage/**",
122
+ "**/htmlcov/**",
123
+ "**/.nyc_output/**",
124
+ "**/test-results/**",
125
+ "**/.coverage/**",
126
+ "**/.idea/**",
127
+ "**/logs/**",
128
+ "**/tmp/**",
129
+ "**/temp/**",
130
+ "**/_build/**",
131
+ "**/docs/_build/**",
132
+ "**/site/**"
133
+ ],
134
+ "languages": [],
135
+ "frameworks": [],
136
+ "maxFileSize": 1048576,
137
+ "extractDocstrings": true,
138
+ "trackCallSites": true,
139
+ "enableEmbeddings": false
140
+ }
@@ -0,0 +1 @@
1
+ QBRIX_API_KEY=
@@ -0,0 +1,28 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR do? 1-3 sentences on the change and motivation. -->
4
+
5
+ ## Type of change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Breaking change
10
+ - [ ] Refactor / internal improvement
11
+ - [ ] Docs / tooling
12
+
13
+ ## Changes
14
+
15
+ <!-- Bullet list of concrete changes made. -->
16
+
17
+ ## Testing
18
+
19
+ - [ ] Existing tests pass (`uv run pytest`)
20
+ - [ ] New tests added for new behaviour
21
+ - [ ] Manually verified against the proxy service
22
+
23
+ ## Checklist
24
+
25
+ - [ ] Public API surface in `__init__.py` updated if needed
26
+ - [ ] Type annotations correct (`uv run mypy qbrix/`)
27
+ - [ ] No new `None` kwargs shadowing env-var config
28
+ - [ ] Breaking changes documented in summary above
@@ -0,0 +1,64 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v4
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ run: uv python install ${{ matrix.python-version }}
24
+
25
+ - name: Install dependencies
26
+ run: uv sync
27
+
28
+ - name: Run tests
29
+ run: uv run pytest --cov=qbrix --cov-report=json
30
+
31
+ - name: Upload coverage artifact
32
+ if: matrix.python-version == '3.13'
33
+ uses: actions/upload-artifact@v4
34
+ with:
35
+ name: coverage-report
36
+ path: coverage.json
37
+
38
+ coverage-badge:
39
+ needs: test
40
+ runs-on: ubuntu-latest
41
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
42
+ steps:
43
+ - name: Download coverage artifact
44
+ uses: actions/download-artifact@v4
45
+ with:
46
+ name: coverage-report
47
+
48
+ - name: Extract coverage percentage
49
+ id: cov
50
+ run: |
51
+ COV=$(python3 -c "import json; print(round(json.load(open('coverage.json'))['totals']['percent_covered']))")
52
+ echo "coverage=$COV" >> "$GITHUB_OUTPUT"
53
+
54
+ - name: Update coverage badge
55
+ uses: schneegans/dynamic-badges-action@v1.7.0
56
+ with:
57
+ auth: ${{ secrets.GIST_SECRET }}
58
+ gistID: ${{ vars.COVERAGE_GIST_ID }}
59
+ filename: qbrix-coverage.json
60
+ label: coverage
61
+ message: ${{ steps.cov.outputs.coverage }}%
62
+ valColorRange: ${{ steps.cov.outputs.coverage }}
63
+ minColorRange: 50
64
+ maxColorRange: 90
@@ -0,0 +1,37 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: astral-sh/setup-uv@v5
15
+
16
+ - run: uv build
17
+
18
+ - uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+
23
+ publish:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ permissions:
28
+ id-token: write
29
+ steps:
30
+ - uses: astral-sh/setup-uv@v5
31
+
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ - run: uv publish dist/*
@@ -0,0 +1,24 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-please:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/create-github-app-token@v1
17
+ id: app-token
18
+ with:
19
+ app-id: ${{ secrets.APP_ID }}
20
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
21
+
22
+ - uses: googleapis/release-please-action@v4
23
+ with:
24
+ token: ${{ steps.app-token.outputs.token }}
qbrix-0.1.3/.gitignore ADDED
@@ -0,0 +1,34 @@
1
+ # python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+
12
+ # venv
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # testing
18
+ .coverage
19
+ htmlcov/
20
+ .pytest_cache/
21
+
22
+ # ide
23
+ .idea/
24
+ .vscode/
25
+ *.swp
26
+ *.swo
27
+ *~
28
+
29
+ # os
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # build
34
+ .pdm-build/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.3"
3
+ }
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## [0.1.3](https://github.com/optiq-io/qbrix-python/compare/v0.1.2...v0.1.3) (2026-03-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * create release github app and integrate ([6ecbf3b](https://github.com/optiq-io/qbrix-python/commit/6ecbf3b2f4c2bc1de61c094913e4b485dc346090))
9
+
10
+ ## [0.1.2](https://github.com/optiq-io/qbrix-python/compare/v0.1.1...v0.1.2) (2026-03-05)
11
+
12
+
13
+ ### Features
14
+
15
+ * created mod client and lazy proxy implementations ([e520223](https://github.com/optiq-io/qbrix-python/commit/e52022372ad48153168a6568f5929c0e6c8b389c))
16
+ * match proxy gate svc endpoint model changes to the sdk ([7d2e0a2](https://github.com/optiq-io/qbrix-python/commit/7d2e0a252f9183a470af88d8559d949da7be3d34))
17
+ * moved the resource file to base ([06dec0d](https://github.com/optiq-io/qbrix-python/commit/06dec0de9718afb0cfe1063142f2cccd80ec8e8a))
18
+ * update base client add deps ([97983d9](https://github.com/optiq-io/qbrix-python/commit/97983d9d531127d88cda44ae3c51fbc17b6e59f2))
19
+ * update ci flow to add automated test coverage ([db5f985](https://github.com/optiq-io/qbrix-python/commit/db5f9851e9c2caf3b8fbd33a8d94e302588dec1f))
20
+
21
+ ## [0.1.1](https://github.com/optiq-io/qbrix-python/compare/v0.1.0...v0.1.1) (2026-03-01)
22
+
23
+
24
+ ### Features
25
+
26
+ * update README.md ([d4106c4](https://github.com/optiq-io/qbrix-python/commit/d4106c4d78fc7515c56093a7cdb9ce1882ba6350))
qbrix-0.1.3/CLAUDE.md ADDED
@@ -0,0 +1,114 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Is
6
+
7
+ Python SDK for the Qbrix distributed computing platform — a multi-armed bandit system for site variant optimisation. The SDK wraps the Qbrix proxy service HTTP API (`proxysvc`), providing typed sync and async clients for pool/experiment/gate management and the agent select/feedback loop.
8
+
9
+ The upstream proxy service lives at `../qbrix/svc/proxy` — consult it for API endpoint behavior, request/response shapes, and feature gate evaluation logic.
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ # Install dependencies (uses uv, not pip)
15
+ uv sync
16
+
17
+ # Run all tests
18
+ uv run pytest
19
+
20
+ # Run a single test file
21
+ uv run pytest tests/test_resource_pool.py
22
+
23
+ # Run a single test by name
24
+ uv run pytest tests/test_resource_pool.py -k "test_create_pool"
25
+
26
+ # Run only unit tests
27
+ uv run pytest -m unit
28
+
29
+ # Run with coverage
30
+ uv run pytest --cov=qbrix
31
+
32
+ # Format
33
+ uv run black .
34
+
35
+ # Type checking (package is PEP 561 typed)
36
+ uv run mypy qbrix/
37
+ ```
38
+
39
+ ## Architecture
40
+
41
+ The SDK follows a layered client → resource → model pattern, with sync and async variants throughout.
42
+
43
+ ### Client Layer (`_base_client.py`, `_client.py`)
44
+
45
+ ```
46
+ Qbrix / AsyncQbrix ← public entry points (users import these)
47
+ ↓ inherits
48
+ SyncAPIClient / AsyncAPIClient ← HTTP logic, retry, error mapping
49
+ ↓ wraps
50
+ httpx.Client / httpx.AsyncClient ← actual HTTP transport
51
+ ```
52
+
53
+ `Qbrix` and `AsyncQbrix` expose resources as `@cached_property` — lazily instantiated on first access. The client itself IS the transport (no separate transport layer, despite what SDK-DESIGN.md describes — that abstraction was collapsed during implementation).
54
+
55
+ **Key divergences from SDK-DESIGN.md:** Client classes are `Qbrix`/`AsyncQbrix` (not `QbrixClient`/`AsyncQbrixClient`). Resource accessors are singular: `client.pool`, `client.experiment`, `client.gate`, `client.agent` (not plural). No `transport/` package exists.
56
+
57
+ ### Resource Layer (`_resource.py`, `resource/`)
58
+
59
+ Each resource file defines both sync and async variants (`PoolResource`/`AsyncPoolResource`, etc.). Resources hold a reference to the client and delegate HTTP calls through `_get`, `_post`, `_put`, `_patch`, `_delete` helpers from `SyncAPIResource`/`AsyncAPIResource`.
60
+
61
+ **`cast_to` pattern:** Resource methods pass `cast_to=ModelClass` to the client's `request()` method, which calls `ModelClass.model_validate(data)` on the JSON response. Methods that don't need a response model (like `delete`, `feedback`) omit `cast_to`.
62
+
63
+ **Dual input types:** All resource methods accept both Pydantic models and raw dicts (e.g., `arms: list[dict | ArmCreate]`, `context: Context | dict`). Serialization uses `isinstance` checks.
64
+
65
+ ### Model Layer (`model/`)
66
+
67
+ All models are Pydantic v2 `BaseModel` subclasses. Request models (e.g., `PoolCreate`, `ExperimentCreate`) and response models (e.g., `Pool`, `Experiment`) are separate classes. `PaginatedResponse[T]` is a generic wrapper with `items`, `limit`, `offset`, and a `has_more` computed property.
68
+
69
+ ### Config (`_config.py`)
70
+
71
+ `QbrixConfig` extends `pydantic-settings.BaseSettings` with `env_prefix="QBRIX_"`. Resolution order: constructor kwargs → env vars (`QBRIX_API_KEY`, `QBRIX_BASE_URL`, etc.) → defaults. The `BaseClient.__init__` filters out `None` kwargs before passing to `QbrixConfig` so that env vars aren't shadowed by explicit `None`.
72
+
73
+ ### Error Handling (`exception.py`)
74
+
75
+ `_base_client._make_status_error()` parses JSON response for `detail` and `context` fields, maps status codes via `STATUS_CODE_TO_EXCEPTION` dict. Unknown status codes fall back to `QbrixAPIError`. `RateLimitedError` parses `Retry-After` header. Network errors map to `QbrixConnectionError`/`QbrixTimeoutError`.
76
+
77
+ ## Proxy API Reference
78
+
79
+ The SDK targets these proxy endpoints (all under `/api/v1`):
80
+
81
+ - **Pools:** `POST/GET/PATCH/DELETE /pools[/{id}]`, `GET /pools/{id}/experiments`
82
+ - **Experiments:** `POST/GET/PATCH/DELETE /experiments[/{id}]` — supports `?search=&enabled=` filters
83
+ - **Gates:** `POST/GET/PUT/DELETE /gates/{experiment_id}` — note: update is `PUT` (full replace), not `PATCH`
84
+ - **Agent:** `POST /agent/select`, `POST /agent/feedback`
85
+
86
+ ### Supported Policies
87
+
88
+ Experiment `policy` field values (must match exactly): `BetaTSPolicy`, `GaussianTSPolicy`, `UCB1TunedPolicy`, `KLUCBPolicy`, `EpsilonPolicy`, `MOSSPolicy`, `MOSSAnyTimePolicy`, `LinUCBPolicy`, `LinTSPolicy`, `EXP3Policy`, `FPLPolicy`.
89
+
90
+ Contextual policies (`LinUCBPolicy`, `LinTSPolicy`) require `context.vector` with length matching the `dim` policy param.
91
+
92
+ ### Agent Select/Feedback Loop
93
+
94
+ 1. `POST /agent/select` → gate evaluation (if configured) → bandit selection → returns `{arm, request_id, is_default}`
95
+ 2. `request_id` is an HMAC-signed opaque token — store it, pass it unchanged to feedback
96
+ 3. `POST /agent/feedback` with `{request_id, reward}` → publishes to learning stream
97
+ 4. `is_default: true` means the gate committed an arm (bypassed bandit)
98
+
99
+ ### Feature Gate Evaluation Order
100
+
101
+ Gate checks: enabled → schedule (date range) → active hours → rollout percentage (hash-based) → rules (first match wins). Any negative check → return `default_arm`, skip bandit.
102
+
103
+ ## Testing Patterns
104
+
105
+ Tests use a `MockSyncClient`/`MockAsyncClient` infrastructure (in `conftest.py`) that subclasses the real API client and replaces the httpx client with a mock. Use `mock_client.enqueue({...})` to stage responses and `mock_client.calls[n]` to assert request method/path/body.
106
+
107
+ Async tests use `@pytest.mark.asyncio` on the class. Test markers: `unit`, `integration`, `slow`.
108
+
109
+ ## Conventions
110
+
111
+ - Internal modules are prefixed with `_` (e.g., `_base_client.py`, `_config.py`, `_resource.py`)
112
+ - Public API surface is defined in `__init__.py` — keep it updated when adding models/exceptions
113
+ - Python ≥ 3.10 required (uses `X | Y` union syntax)
114
+ - Dependencies: `httpx`, `pydantic`, `pydantic-settings` (runtime); `pytest`, `pytest-asyncio`, `pytest-mock`, `pytest-cov`, `black`, `pre-commit` (dev)
qbrix-0.1.3/PKG-INFO ADDED
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: qbrix
3
+ Version: 0.1.3
4
+ Summary: Python SDK for the qbrix multi-armed bandit platform
5
+ Author-email: Optiq <hello@qbrix.io>
6
+ License-Expression: MIT
7
+ Keywords: a/b testing,bandit,experimentation,mab,optimization
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: click>=8.3.1
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: pydantic-settings>=2.13.1
20
+ Requires-Dist: pydantic>=2.12.5
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <img src="./asset/logo/bb_logo.svg" alt="Qbrix" width="280">
25
+ </p>
26
+
27
+ <p align="center">
28
+ <strong>Python SDK for the qbrix platform.</strong>
29
+ </p>
30
+
31
+ <p align="center">
32
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
33
+ <img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/eskinmi/c7d91705ef877065365d0febc49e0ea9/raw/qbrix-coverage.json" alt="Coverage">
34
+ <img src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python 3.10+">
35
+ <img src="https://img.shields.io/badge/pydantic-v2-e92063?logo=pydantic&logoColor=white" alt="Pydantic v2">
36
+ <img src="https://img.shields.io/badge/httpx-async%20%2B%20sync-1e88e5" alt="httpx">
37
+ </p>
38
+
39
+
40
+ ---
41
+
42
+ Typed sync and async clients for [Qbrix](https://github.com/optiq-io/qbrix) — pool/experiment/gate management and the agent select/feedback loop.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install qbrix
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ Set your credentials as environment variables and call resources directly — no client instantiation needed:
53
+
54
+ ```bash
55
+ export QBRIX_API_KEY="optiq_xxx"
56
+ export QBRIX_BASE_URL="https://api.qbrix.io"
57
+ ```
58
+
59
+ ```python
60
+ import qbrix
61
+
62
+ # 1. Create a pool of arms (variants)
63
+ pool = qbrix.pool.create(
64
+ name="homepage-buttons",
65
+ arms=[{"name": "blue"}, {"name": "green"}, {"name": "red"}],
66
+ )
67
+
68
+ # 2. Create an experiment with a bandit policy
69
+ exp = qbrix.experiment.create(
70
+ name="button-color-test",
71
+ pool_id=pool.id,
72
+ policy="BetaTSPolicy",
73
+ )
74
+
75
+ # 3. Select an arm for a user
76
+ result = qbrix.agent.select(
77
+ experiment_id=exp.id,
78
+ context={"id": "user-123", "metadata": {"country": "US"}},
79
+ )
80
+ print(result.arm.name) # "green"
81
+ print(result.is_default) # False (bandit selected)
82
+
83
+ # 4. Send feedback (reward) after observing the outcome
84
+ qbrix.agent.feedback(request_id=result.request_id, reward=1.0)
85
+ ```
86
+
87
+ The system learns from every reward and adjusts future selections automatically.
88
+
89
+ ## Explicit Client
90
+
91
+ For full control over configuration or lifecycle (e.g. closing the HTTP connection, using a context manager), instantiate the client directly:
92
+
93
+ ```python
94
+ from qbrix import Qbrix
95
+
96
+ with Qbrix(api_key="optiq_xxx", base_url="https://api.qbrix.io") as client:
97
+ pool = client.pool.create(
98
+ name="homepage-buttons",
99
+ arms=[{"name": "blue"}, {"name": "green"}, {"name": "red"}],
100
+ )
101
+ result = client.agent.select(experiment_id="exp-uuid", context={"id": "user-123"})
102
+ client.agent.feedback(request_id=result.request_id, reward=1.0)
103
+ ```
104
+
105
+ ## Async
106
+
107
+ ```python
108
+ from qbrix import AsyncQbrix
109
+
110
+ async with AsyncQbrix(api_key="optiq_xxx") as client:
111
+ result = await client.agent.select(
112
+ experiment_id="exp-uuid",
113
+ context={"id": "user-456"},
114
+ )
115
+ await client.agent.feedback(request_id=result.request_id, reward=1.0)
116
+ ```
117
+
118
+ ## Configuration
119
+
120
+ Constructor kwargs take priority over environment variables, which take priority over defaults.
121
+
122
+ ```bash
123
+ export QBRIX_API_KEY="optiq_xxx"
124
+ export QBRIX_BASE_URL="https://api.qbrix.io"
125
+ ```
126
+
127
+ ```python
128
+ from qbrix import Qbrix
129
+
130
+ client = Qbrix() # picks up env vars automatically
131
+ ```
132
+
133
+ | Env Var | Default | Description |
134
+ |---------|---------|-------------|
135
+ | `QBRIX_API_KEY` | `None` | API key (`optiq_xxx`) |
136
+ | `QBRIX_BASE_URL` | `http://localhost:8080` | Proxy service URL |
137
+ | `QBRIX_TIMEOUT` | `30.0` | Request timeout (seconds) |
138
+ | `QBRIX_MAX_RETRIES` | `3` | Retry count on 429/5xx |
139
+
140
+ ## Feature Gates
141
+
142
+ Attach a feature gate to control rollout before the bandit kicks in:
143
+
144
+ ```python
145
+ import qbrix
146
+
147
+ qbrix.gate.create(
148
+ experiment_id=exp.id,
149
+ enabled=True,
150
+ rollout_percentage=80.0,
151
+ default_arm_id=pool.arms[0].id,
152
+ rules=[
153
+ {"key": "plan", "operator": "==", "value": "enterprise", "arm_id": pool.arms[1].id},
154
+ ],
155
+ )
156
+
157
+ # Gate-matched selections return is_default=True
158
+ result = qbrix.agent.select(
159
+ experiment_id=exp.id,
160
+ context={"id": "user-789", "metadata": {"plan": "enterprise"}},
161
+ )
162
+ print(result.is_default) # True
163
+ ```
164
+
165
+ ## Error Handling
166
+
167
+ ```python
168
+ import qbrix
169
+ from qbrix import NotFoundError, RateLimitedError
170
+
171
+ try:
172
+ exp = qbrix.experiment.get("nonexistent-id")
173
+ except NotFoundError as e:
174
+ print(f"Not found: {e.detail}")
175
+ except RateLimitedError as e:
176
+ print(f"Retry after {e.retry_after}s")
177
+ ```
178
+
179
+ ## Supported Policies
180
+
181
+ | Policy | Type | Best For |
182
+ |--------------------|-------------|--------------------------------------|
183
+ | `BetaTSPolicy` | Stochastic | Binary rewards (clicks, conversions) |
184
+ | `GaussianTSPolicy` | Stochastic | Continuous rewards |
185
+ | `UCB1TunedPolicy` | Stochastic | Theoretical regret guarantees |
186
+ | `KLUCBPolicy` | Stochastic | Binary rewards with tight bounds |
187
+ | `MOSSPolicy` | Stochastic | Fixed horizon problems |
188
+ | `LinUCBPolicy` | Contextual | Linear reward models with features |
189
+ | `LinTSPolicy` | Contextual | Linear models with uncertainty |
190
+ | `EXP3Policy` | Adversarial | Non-stationary environments |
191
+ | `FPLPolicy` | Adversarial | Follow the perturbed leader |
192
+
193
+ ## License
194
+
195
+ [MIT](LICENSE)