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.
- qbrix-0.1.3/.codegraph/.gitignore +16 -0
- qbrix-0.1.3/.codegraph/config.json +140 -0
- qbrix-0.1.3/.env.example +1 -0
- qbrix-0.1.3/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- qbrix-0.1.3/.github/workflows/ci.yml +64 -0
- qbrix-0.1.3/.github/workflows/publish.yml +37 -0
- qbrix-0.1.3/.github/workflows/release-please.yml +24 -0
- qbrix-0.1.3/.gitignore +34 -0
- qbrix-0.1.3/.release-please-manifest.json +3 -0
- qbrix-0.1.3/CHANGELOG.md +26 -0
- qbrix-0.1.3/CLAUDE.md +114 -0
- qbrix-0.1.3/PKG-INFO +195 -0
- qbrix-0.1.3/README.md +173 -0
- qbrix-0.1.3/asset/logo/bb_logo.svg +13 -0
- qbrix-0.1.3/bin/.keep +0 -0
- qbrix-0.1.3/examples/quickstart.py +142 -0
- qbrix-0.1.3/pyproject.toml +54 -0
- qbrix-0.1.3/qbrix/__init__.py +91 -0
- qbrix-0.1.3/qbrix/_base_client.py +279 -0
- qbrix-0.1.3/qbrix/_client.py +72 -0
- qbrix-0.1.3/qbrix/_config.py +16 -0
- qbrix-0.1.3/qbrix/_mod_client.py +22 -0
- qbrix-0.1.3/qbrix/_proxies.py +33 -0
- qbrix-0.1.3/qbrix/_util.py +25 -0
- qbrix-0.1.3/qbrix/_version.py +1 -0
- qbrix-0.1.3/qbrix/exception.py +90 -0
- qbrix-0.1.3/qbrix/model/__init__.py +41 -0
- qbrix-0.1.3/qbrix/model/agent.py +31 -0
- qbrix-0.1.3/qbrix/model/auth.py +13 -0
- qbrix-0.1.3/qbrix/model/common.py +27 -0
- qbrix-0.1.3/qbrix/model/experiment.py +35 -0
- qbrix-0.1.3/qbrix/model/gate.py +32 -0
- qbrix-0.1.3/qbrix/model/pool.py +35 -0
- qbrix-0.1.3/qbrix/py.typed +0 -0
- qbrix-0.1.3/qbrix/resource/__init__.py +19 -0
- qbrix-0.1.3/qbrix/resource/_base.py +105 -0
- qbrix-0.1.3/qbrix/resource/agent.py +68 -0
- qbrix-0.1.3/qbrix/resource/experiment.py +167 -0
- qbrix-0.1.3/qbrix/resource/gate.py +184 -0
- qbrix-0.1.3/qbrix/resource/pool.py +108 -0
- qbrix-0.1.3/release-please-config.json +11 -0
- qbrix-0.1.3/tests/__init__.py +0 -0
- qbrix-0.1.3/tests/conftest.py +104 -0
- qbrix-0.1.3/tests/test_base_client.py +252 -0
- qbrix-0.1.3/tests/test_client.py +82 -0
- qbrix-0.1.3/tests/test_config.py +40 -0
- qbrix-0.1.3/tests/test_exception.py +86 -0
- qbrix-0.1.3/tests/test_models.py +210 -0
- qbrix-0.1.3/tests/test_resource_agent.py +107 -0
- qbrix-0.1.3/tests/test_resource_experiment.py +130 -0
- qbrix-0.1.3/tests/test_resource_gate.py +106 -0
- qbrix-0.1.3/tests/test_resource_pool.py +137 -0
- qbrix-0.1.3/uv.lock +869 -0
|
@@ -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
|
+
}
|
qbrix-0.1.3/.env.example
ADDED
|
@@ -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/
|
qbrix-0.1.3/CHANGELOG.md
ADDED
|
@@ -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)
|