tipsy-ab-config 0.15.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.
- tipsy_ab_config-0.15.0/CHANGELOG.md +424 -0
- tipsy_ab_config-0.15.0/LICENSE +21 -0
- tipsy_ab_config-0.15.0/MANIFEST.in +12 -0
- tipsy_ab_config-0.15.0/PKG-INFO +268 -0
- tipsy_ab_config-0.15.0/README.md +227 -0
- tipsy_ab_config-0.15.0/pyproject.toml +73 -0
- tipsy_ab_config-0.15.0/setup.cfg +4 -0
- tipsy_ab_config-0.15.0/tests/test_abtest_context.py +363 -0
- tipsy_ab_config-0.15.0/tests/test_abtest_context_traceid.py +204 -0
- tipsy_ab_config-0.15.0/tests/test_auth_interceptor.py +198 -0
- tipsy_ab_config-0.15.0/tests/test_backoff_reset.py +97 -0
- tipsy_ab_config-0.15.0/tests/test_cache.py +232 -0
- tipsy_ab_config-0.15.0/tests/test_enrollment_alignment.py +183 -0
- tipsy_ab_config-0.15.0/tests/test_enrollment_log.py +688 -0
- tipsy_ab_config-0.15.0/tests/test_enrollment_merge.py +353 -0
- tipsy_ab_config-0.15.0/tests/test_fastapi.py +421 -0
- tipsy_ab_config-0.15.0/tests/test_fastapi_middleware_traceid.py +250 -0
- tipsy_ab_config-0.15.0/tests/test_get_all_configs.py +492 -0
- tipsy_ab_config-0.15.0/tests/test_get_config.py +337 -0
- tipsy_ab_config-0.15.0/tests/test_get_experiment_result_traceid.py +188 -0
- tipsy_ab_config-0.15.0/tests/test_grpc_target.py +523 -0
- tipsy_ab_config-0.15.0/tests/test_heartbeat.py +124 -0
- tipsy_ab_config-0.15.0/tests/test_http_transport.py +1024 -0
- tipsy_ab_config-0.15.0/tests/test_metrics.py +37 -0
- tipsy_ab_config-0.15.0/tests/test_new_abtest_context_traceid.py +199 -0
- tipsy_ab_config-0.15.0/tests/test_packaging.py +199 -0
- tipsy_ab_config-0.15.0/tests/test_prefetch.py +255 -0
- tipsy_ab_config-0.15.0/tests/test_pull.py +170 -0
- tipsy_ab_config-0.15.0/tests/test_pull_subscribe_traceid.py +242 -0
- tipsy_ab_config-0.15.0/tests/test_rpc_timing_log.py +258 -0
- tipsy_ab_config-0.15.0/tests/test_secret_key_auth.py +355 -0
- tipsy_ab_config-0.15.0/tests/test_service_config.py +252 -0
- tipsy_ab_config-0.15.0/tests/test_subscribe.py +138 -0
- tipsy_ab_config-0.15.0/tests/test_subscribe_backoff_reset.py +176 -0
- tipsy_ab_config-0.15.0/tests/test_typed_config.py +413 -0
- tipsy_ab_config-0.15.0/tests/test_v2_namespace.py +443 -0
- tipsy_ab_config-0.15.0/tests/test_version_sync.py +129 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/__init__.py +53 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_http_transport.py +159 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/__init__.py +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/__init__.py +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/__init__.py +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/v1/__init__.py +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/v1/abtest_pb2.py +85 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/v1/abtest_pb2_grpc.py +219 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/__init__.py +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/v1/__init__.py +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/v1/config_pb2.py +113 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/v1/config_pb2_grpc.py +523 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/abtest_context.py +318 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/cache.py +231 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/client.py +2233 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/exceptions.py +46 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/fastapi_middleware.py +176 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/metrics.py +96 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config/py.typed +0 -0
- tipsy_ab_config-0.15.0/tipsy_ab_config.egg-info/SOURCES.txt +54 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `tipsy-ab-config` Python SDK are documented in
|
|
4
|
+
this file.
|
|
5
|
+
|
|
6
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.15.0] - 2026-08-27
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `Config.secret_key`:仅配置 secretKey(即 `TIPSY_SERVICE_SECRET` 本身)即可完成鉴权(issue #16,
|
|
15
|
+
需平台侧配套 verifier)。凭据优先级 **SecretKey > token_provider > token**(每请求求值);
|
|
16
|
+
发送形态为 `Authorization: SecretKey <secret>`(gRPC metadata 与 HTTP header 双路径一致,
|
|
17
|
+
SDK 恒发精确字面量,服务端按 RFC 7235 对 scheme 大小写不敏感匹配)。Init 校验放宽为
|
|
18
|
+
`secret_key / token / token_provider` 至少其一(`channel_factory` 豁免语义不变:工厂通道
|
|
19
|
+
绕开 SDK auth 插桩,secret_key 同样不附着,无需凭据)。SDK 不读任何环境变量,secretKey 由
|
|
20
|
+
业务方显式传入。secretKey 校验通过 = 全量访问(等效 `internal_service` + `"*"`),信任边界
|
|
21
|
+
见 `docs/usage-and-integration.md` §3。
|
|
22
|
+
- `get_config` / `get_config_default` 命中日志(`get_config hit (abtest)` / `(full)`,msg 文本不变)
|
|
23
|
+
新增结构化归因字段:恒定输出 `reason`(枚举 4 值:`full_release` / `experiment` /
|
|
24
|
+
`gray_whitelist` / `abtest_unattributed`)与 `trace_id`(Python 端本次补齐,取自
|
|
25
|
+
AbtestContext,对齐 Go/Java);`reason=experiment` 时另输出 `experiment_id` / `group_id`,
|
|
26
|
+
`reason=gray_whitelist` 时另输出 `release_id`(条件字段缺席即 omit,不输出键)。既有字段
|
|
27
|
+
`ns` / `key` / `version` / `uid` 不变。`get_all_configs` 不打每-key 命中日志(其聚合 Debug
|
|
28
|
+
日志不变);`get_config_static` 行为与日志不变(无 reason,不属于入组事件)。
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- 内部 per-ns 取数(`_fetch_config_version_flat_kv_for_ns`)请求的 display_type 由
|
|
32
|
+
`FLAT_KV` 切换为 `EACH_EXPERIMENT_GROUP`(experiment_type 仍为 `CONFIG_VERSION`),
|
|
33
|
+
SDK 按平台 flat 合并规则本地重建 key→versionId map(灰度无条件优先于实验;实验组间
|
|
34
|
+
后写覆盖;灰度间 first-writer-wins),值解析结果与原 FLAT_KV 语义等价,同时保留服务端
|
|
35
|
+
折叠前的归因信息供命中日志使用。公共 API(`get_experiment_result` 及含 "flat_kv" 的
|
|
36
|
+
方法名)签名与语义不变。
|
|
37
|
+
- ab→full fallback 的 WARN 日志(`ab version missing in local cache`)补上 `trace_id`。
|
|
38
|
+
|
|
39
|
+
## [0.14.2] - 2026-08-24
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- `get_config` / `get_config_default` / `get_config_static` 命中日志从 `DEBUG` 提升到 `INFO`
|
|
43
|
+
(字段 `ns`/`key`/`version`,动态路径另带 `uid`)。目的:在默认 `INFO` 级别下即可
|
|
44
|
+
从调用方日志观测业务实际请求的 config key 与命中版本,无需临时开 DEBUG。
|
|
45
|
+
出于泄漏风险,命中日志**不记录 config value**。
|
|
46
|
+
|
|
47
|
+
## [0.14.1] - 2026-08-10
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **`ConfigService.Subscribe` was sent with no `authorization` header, so every
|
|
52
|
+
subscribe attempt was rejected with `Unauthenticated: missing authorization
|
|
53
|
+
metadata`** ([#8](https://github.com/Lightspeed-Intelligence/tipsy-ab-config-sdk/issues/8)).
|
|
54
|
+
The bearer interceptor was a single class inheriting both
|
|
55
|
+
`grpc.aio.UnaryUnaryClientInterceptor` and
|
|
56
|
+
`grpc.aio.UnaryStreamClientInterceptor`; grpcio sorts interceptors into
|
|
57
|
+
per-method-kind lists with an if/elif chain, so the one instance was only ever
|
|
58
|
+
registered as unary-unary and the unary-stream list stayed empty. Unary calls
|
|
59
|
+
(`PullAll`, `GetExperimentResult`) authenticated normally, which is why this
|
|
60
|
+
was not an outage: change detection silently degraded from Subscribe push to
|
|
61
|
+
`pull_interval` polling (default 10s staleness) while the log filled with one
|
|
62
|
+
`Subscribe stream error; reconnecting` ERROR per reconnect, indefinitely. Fixed
|
|
63
|
+
by splitting into two single-purpose interceptors registered separately.
|
|
64
|
+
Affects the plaintext and TLS paths equally. Go and Java are unaffected — they
|
|
65
|
+
use `PerRPCCredentials` / `CallCredentials`, which apply to all RPC kinds.
|
|
66
|
+
|
|
67
|
+
**Upgrade note.** If you run in gRPC mode and have been seeing repeated
|
|
68
|
+
`Subscribe stream error; reconnecting` ERROR lines, this release is the fix:
|
|
69
|
+
config-change push starts working again and that log noise stops. No code or
|
|
70
|
+
config change is needed on your side.
|
|
71
|
+
|
|
72
|
+
- **Declared dependency floors were not installable.** `grpcio>=1.60` disagreed
|
|
73
|
+
with the checked-in generated stubs, which carry
|
|
74
|
+
`GRPC_GENERATED_VERSION = '1.66.2'` and raise `RuntimeError` at import on
|
|
75
|
+
anything lower — so a consumer resolving to the declared floor (an explicit
|
|
76
|
+
pin, an old lockfile, a conservative resolver) crashed on
|
|
77
|
+
`import tipsy_ab_config`. Raised to `grpcio>=1.66.2,<2`, which is the floor the
|
|
78
|
+
stubs actually enforce; wheels exist for every supported Python (3.10–3.13), so
|
|
79
|
+
no platform support is lost. Separately, `protobuf>=5.29` named a floor that
|
|
80
|
+
resolves to the **yanked** 5.29.0 when pinned exactly
|
|
81
|
+
([protocolbuffers/protobuf#19430](https://github.com/protocolbuffers/protobuf/issues/19430));
|
|
82
|
+
raised to `>=5.29.1,<7`. No runtime behaviour change — this only corrects
|
|
83
|
+
metadata that was wrong, plus the matching README compatibility table.
|
|
84
|
+
A new `min-deps` CI job installs the declared floors and runs the full suite
|
|
85
|
+
against them (the existing matrix always resolves to the newest release, which
|
|
86
|
+
is why this escaped), and `tests/test_version_sync.py` now derives the expected
|
|
87
|
+
`grpcio` floor from the stubs so the two cannot silently diverge again.
|
|
88
|
+
|
|
89
|
+
**Upgrade note.** If your project pins `grpcio` or `protobuf` below these
|
|
90
|
+
floors, raise the pins when you upgrade. The floors were always what the
|
|
91
|
+
shipped stubs required — the declared metadata was simply wrong.
|
|
92
|
+
|
|
93
|
+
## [0.14.0] - 2026-07-28
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- `Client.get_all_configs(ctx, namespace)` and
|
|
98
|
+
`Client.get_all_configs_default(ctx)` — resolve **every** dynamic config under
|
|
99
|
+
a `(namespace, user)` into a fresh `dict[str, str]` mapping key → value, using
|
|
100
|
+
the exact same resolution logic as `get_config` (abtest whitelist / experiment
|
|
101
|
+
hit > full-release fallback), just without a specific `key`. A key that
|
|
102
|
+
resolves to neither an abtest hit nor a full-release value is **omitted** from
|
|
103
|
+
the map (no value = not present; a genuinely empty-string value is still
|
|
104
|
+
returned). A subscribed-but-not-yet-pulled namespace (no snapshot) returns an
|
|
105
|
+
empty `dict` with zero RPC. The per-ns abtest result is shared with
|
|
106
|
+
`get_config`, so the whole request link issues **at most one**
|
|
107
|
+
`GetExperimentResult` RPC per namespace; when every key in the namespace is
|
|
108
|
+
explicitly pure-full-release (`has_dynamic_resolution` present and `False`) the
|
|
109
|
+
abtest wait is skipped entirely (zero RPC). `get_all_configs_default` resolves
|
|
110
|
+
the project default namespace. Mirrors the Go and Java SDKs.
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
|
|
114
|
+
- **A `user_id` of `""` or `"0"` is now treated as identity-less: no
|
|
115
|
+
`GetExperimentResult` RPC, static resolution only.** Both `get_config` and
|
|
116
|
+
`get_all_configs` for such a context skip the experiment / whitelist logic
|
|
117
|
+
entirely (no RPC) and resolve straight to the full-release value — for
|
|
118
|
+
`get_config` the default when no full release exists; for `get_all_configs`
|
|
119
|
+
the key is dropped. The short-circuit lives in the lazy-fetch layer, so
|
|
120
|
+
`prefetch_config_version_flat_kv_for_namespace` / `wait_for_abtest` also issue
|
|
121
|
+
no RPC for these uids (behaving like `empty_abtest_context`). A pre-seeded
|
|
122
|
+
`mock_abtest_context(user_id="", …)` result still wins. Any other uid is
|
|
123
|
+
unchanged. The `"0"` value is called out explicitly because it is a common
|
|
124
|
+
"no logged-in user" sentinel. Previously both `""` and `"0"` sent a
|
|
125
|
+
`GetExperimentResult` RPC with an empty/zero user_id, which has no meaningful
|
|
126
|
+
experiment bucketing. Mirrors the Go and Java SDKs.
|
|
127
|
+
- `AbtestContext(user_id=None)` is now normalised to `""` at construction
|
|
128
|
+
(parity with the Java constructor), so a `None` uid takes the identity-less
|
|
129
|
+
short-circuit above instead of reaching proto encoding.
|
|
130
|
+
|
|
131
|
+
## [0.13.0] - 2026-07-23
|
|
132
|
+
|
|
133
|
+
### Changed
|
|
134
|
+
|
|
135
|
+
- **env is now judged server-side; the SDK no longer sends an env request
|
|
136
|
+
field.** env matching moved to the abConfig server, which reads its own
|
|
137
|
+
`TIPSY_ENV` process environment variable and compares it against each
|
|
138
|
+
experiment's env set. Removed `Config.env`; the four request construction
|
|
139
|
+
points (`get_experiment_result`, the `config_version` flat_kv fetch behind
|
|
140
|
+
`get_config`, `_pull_once`, `_subscribe_once`) no longer set the request's
|
|
141
|
+
`env` field.
|
|
142
|
+
|
|
143
|
+
### Compatibility
|
|
144
|
+
|
|
145
|
+
- The `env` field remains in the vendored proto bindings (`abtest_pb2.py` /
|
|
146
|
+
`config_pb2.py`) and is left unset by this SDK — retained for wire
|
|
147
|
+
compatibility with the previously released 0.12.0 and now deprecated on the
|
|
148
|
+
request path. Newer servers ignore any request-supplied env and use
|
|
149
|
+
`TIPSY_ENV` instead, so both old (0.12.0, still sending env) and new SDK
|
|
150
|
+
builds interoperate with the new server. The proto bindings are unchanged (no
|
|
151
|
+
regen).
|
|
152
|
+
|
|
153
|
+
## [0.12.0] - 2026-07-22
|
|
154
|
+
|
|
155
|
+
### Added
|
|
156
|
+
|
|
157
|
+
- `Config.env` (`str`, default `""`) — a single-value environment identifier
|
|
158
|
+
stamped onto **every** outbound request: `GetExperimentResult`, the
|
|
159
|
+
`config_version` flat_kv fetch behind `get_config`, the background `PullAll`,
|
|
160
|
+
and the `Subscribe` stream. It tells the server which environment this
|
|
161
|
+
process runs in so experiment env filtering can apply (an experiment with a
|
|
162
|
+
non-empty env set is only entered when this env is a member; `env=""` enters
|
|
163
|
+
only experiments that do not restrict their env). No `os.getenv` fallback —
|
|
164
|
+
the value is used verbatim. Regenerates the vendored proto bindings
|
|
165
|
+
(`abtest_pb2.py` / `config_pb2.py`) to include the `env` field on the five
|
|
166
|
+
SDK-facing request messages. Mirrors the Go and Java SDKs.
|
|
167
|
+
|
|
168
|
+
### Compatibility
|
|
169
|
+
|
|
170
|
+
- 100% backwards compatible. `env` defaults to `""`; the HTTP transport's
|
|
171
|
+
`MessageToJson` omits an empty scalar, so an unset `env` is byte-for-byte
|
|
172
|
+
wire-compatible with older servers. A configured env sent to an older server
|
|
173
|
+
that predates the field is safely ignored.
|
|
174
|
+
|
|
175
|
+
## [0.11.0] - 2026-07-16
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
|
|
179
|
+
- Ignore Subscribe `Heartbeat` events (forward-compat): the server may emit
|
|
180
|
+
liveness-only `Heartbeat` frames on an otherwise-idle Subscribe stream. The
|
|
181
|
+
SDK now handles them as an explicit no-op — no cache mutation, no sequence
|
|
182
|
+
tracking, no metric — and regenerates the vendored proto bindings
|
|
183
|
+
(`config_pb2.py`) to include the `Heartbeat` message. Mirrors the Go SDK.
|
|
184
|
+
|
|
185
|
+
### Changed
|
|
186
|
+
|
|
187
|
+
- Reset the Subscribe reconnect backoff after a healthy connection drop: a
|
|
188
|
+
stream that stayed up at least 60s before dropping now reconnects at the
|
|
189
|
+
initial delay instead of continuing the exponential climb. A short-lived
|
|
190
|
+
connection still backs off exponentially. Mirrors the Go SDK.
|
|
191
|
+
|
|
192
|
+
## [0.10.0] - 2026-07-03
|
|
193
|
+
|
|
194
|
+
### Added
|
|
195
|
+
|
|
196
|
+
- Debug-level per-call timing log for `GetExperimentResult`. Both call
|
|
197
|
+
sites (the public `get_experiment_result` and the `AbtestContext` lazy
|
|
198
|
+
per-namespace fetch) emit one Debug record per RPC with `ns`, `trace_id`
|
|
199
|
+
and float-millisecond `duration_ms` (an `err` field on failure). Fields
|
|
200
|
+
are written both into the log message and `extra=` so they are visible
|
|
201
|
+
under the default formatter and available as structured record
|
|
202
|
+
attributes. Info level stays silent; the existing fallback warnings are
|
|
203
|
+
unchanged. Mirrors the Go SDK v0.9.0 change.
|
|
204
|
+
|
|
205
|
+
## [0.9.0] - 2026-06-30
|
|
206
|
+
|
|
207
|
+
### Added
|
|
208
|
+
|
|
209
|
+
- Typed config accessors, mirroring the Go SDK's v0.7.0 surface. Static
|
|
210
|
+
(sync, cache-only) and dynamic (async, abtest-resolved) variants for each
|
|
211
|
+
scalar type plus JSON:
|
|
212
|
+
- Static: `get_config_static_bool/long/double/string/json(namespace, key,
|
|
213
|
+
default=...)`.
|
|
214
|
+
- Dynamic: `async get_config_bool/long/double/string/json(ctx, namespace,
|
|
215
|
+
key, default=...)`.
|
|
216
|
+
- **Bool is lenient and never raises**: the stripped value casefold-equal to
|
|
217
|
+
`"true"` or equal to `"1"` ⇒ `True`, everything else ⇒ `False`. The console
|
|
218
|
+
writes canonical `true`/`false`, so the write-strict / read-lenient
|
|
219
|
+
asymmetry is deliberate.
|
|
220
|
+
- **Long uses `int(str)`**, which is arbitrary-precision in Python, so values
|
|
221
|
+
beyond 2^53 are lossless.
|
|
222
|
+
- Miss and value-parse failure both fall back to the supplied default; the
|
|
223
|
+
underlying `get_config` exceptions (`SDKClosed` / `NamespaceRequired` /
|
|
224
|
+
`NamespaceNotSubscribed` / abtest-context) still propagate — only
|
|
225
|
+
`ValueError` / `json.JSONDecodeError` from parsing are swallowed.
|
|
226
|
+
- The config value stays a canonical string end-to-end; the declared
|
|
227
|
+
`value_type` is a console-side write contract and is not carried to the SDK.
|
|
228
|
+
|
|
229
|
+
## [0.8.0] - 2026-07-01
|
|
230
|
+
|
|
231
|
+
### Changed (BREAKING)
|
|
232
|
+
|
|
233
|
+
- **`GetExperimentResultResponse.gray_hits` is now grouped per gray release.**
|
|
234
|
+
`GrayReleaseHit` changed from the flat `{release_id, key, version_id}` (one
|
|
235
|
+
entry per `(release, key)`) to `{release_id, map<string,int64> key_versions}`
|
|
236
|
+
(one entry per hit `release_id`; `key_versions` maps each `config_key.key`
|
|
237
|
+
name → versionId). This aligns `gray_hits` with
|
|
238
|
+
`ExperimentGroupResult.params_versions`. Read a single key's target via
|
|
239
|
+
`gray_hits[i].key_versions[key_name]` instead of the removed
|
|
240
|
+
`gray_hits[i].version_id`. No backward compatibility — the old flat fields
|
|
241
|
+
are gone. The int64 values remain versionId (config_version PRIMARY KEY id,
|
|
242
|
+
globally unique), never the semantic version_no.
|
|
243
|
+
|
|
244
|
+
## [0.7.0] - 2026-06-27
|
|
245
|
+
|
|
246
|
+
### Added
|
|
247
|
+
|
|
248
|
+
- `KeyState.has_dynamic_resolution` (`Optional[bool]`) — the SDK now
|
|
249
|
+
deserializes the new `optional bool has_dynamic_resolution` field on each
|
|
250
|
+
`KeyState` from both the `PullAll` and `Subscribe` snapshot paths. The value
|
|
251
|
+
preserves wire presence: `True`/`False` when the server explicitly set it,
|
|
252
|
+
`None` when the field was absent (an older server that predates it).
|
|
253
|
+
- `ConfigCache.has_dynamic_resolution(namespace, key) -> tuple[bool, bool]` —
|
|
254
|
+
cache accessor returning `(value, present)`; `(False, False)` on any miss
|
|
255
|
+
(no snapshot, no such key, or the field absent on the wire).
|
|
256
|
+
|
|
257
|
+
### Changed
|
|
258
|
+
|
|
259
|
+
- `Client.get_config` fast path: when the server has explicitly reported that a
|
|
260
|
+
key carries no gray release / experiment (`has_dynamic_resolution` is present
|
|
261
|
+
**and** `False`), `get_config` now **skips** the `GetExperimentResult` abtest
|
|
262
|
+
wait and returns the full-release value directly. This removes a guaranteed-
|
|
263
|
+
wasted RPC for pure-full-release keys. All fallback/default semantics and
|
|
264
|
+
metrics are unchanged. When the field is absent (old server) or `True`, the
|
|
265
|
+
existing always-wait abtest path is preserved exactly, so gray release /
|
|
266
|
+
experiments never silently break.
|
|
267
|
+
|
|
268
|
+
### Server compatibility (REQUIRED — upgrade server first)
|
|
269
|
+
|
|
270
|
+
- This version expects the server to already emit the
|
|
271
|
+
`has_dynamic_resolution` field on `KeyState`, i.e. a server built against
|
|
272
|
+
**`api/gen/go` v0.3.0 or newer**. **Upgrade the server first, then this
|
|
273
|
+
SDK.** If this SDK is pointed at an older server that does not emit the
|
|
274
|
+
field, it safely falls back to the previous behaviour — it ALWAYS waits on
|
|
275
|
+
the abtest result (correct results; gray release / experiments keep working),
|
|
276
|
+
it just does not get the fast-path benefit. The fast path is only taken when
|
|
277
|
+
the field is explicitly `False`, never when it is absent, so a new SDK on an
|
|
278
|
+
old server never wrongly skips abtest.
|
|
279
|
+
|
|
280
|
+
## [0.6.0] - 2026-06-25
|
|
281
|
+
|
|
282
|
+
### Changed (BREAKING)
|
|
283
|
+
|
|
284
|
+
- `Client.new_abtest_context` and `Client.abtest_scope` no longer accept a
|
|
285
|
+
`namespace` parameter. Construction is now a pure create and issues **no**
|
|
286
|
+
`GetExperimentResult` RPC. Every namespace is fetched lazily and memoised
|
|
287
|
+
at-most-once on first dynamic `get_config`. **Latency-shape change:** the
|
|
288
|
+
default namespace is no longer eagerly pre-fetched at construction, so the
|
|
289
|
+
first `get_config` for it now pays the GetExperimentResult RPC latency
|
|
290
|
+
inline (previously that RPC was overlapped with construction).
|
|
291
|
+
- `AbtestMiddleware` no longer auto-prefetches on every request. It gained a
|
|
292
|
+
`prefetch_paths: Optional[Sequence[str]] = None` parameter (an exact-match
|
|
293
|
+
URL whitelist). The **default is no prefetch**; only requests whose
|
|
294
|
+
`request.url.path` exactly matches a whitelisted path warm the default
|
|
295
|
+
namespace. This removes the "every request through the middleware fires a
|
|
296
|
+
possibly-wasted experiment RPC" behaviour.
|
|
297
|
+
|
|
298
|
+
### Added
|
|
299
|
+
|
|
300
|
+
- `AbtestContext.prefetch_config_version_flat_kv_for_namespace(ns)` — explicit,
|
|
301
|
+
opt-in, single-namespace prefetch API. Non-blocking and idempotent; spawns
|
|
302
|
+
the single per-ns `GetExperimentResult` (config_version + flat_kv) task so a
|
|
303
|
+
later `get_config` for that namespace reuses it (at-most-once). Empty /
|
|
304
|
+
identity-less / mock contexts and unsubscribed namespaces short-circuit
|
|
305
|
+
without issuing any RPC. Requires a running asyncio event loop.
|
|
306
|
+
|
|
307
|
+
### Internal
|
|
308
|
+
|
|
309
|
+
- Renamed the internal `Client._get_experiment_result_for_ns` →
|
|
310
|
+
`Client._fetch_config_version_flat_kv_for_ns` (hardwired config_version +
|
|
311
|
+
flat_kv), to disambiguate it from the public general-purpose
|
|
312
|
+
`Client.get_experiment_result` (unchanged). The slot-creation logic moved
|
|
313
|
+
into a single shared `AbtestContext._ensure_fetch` primitive (synchronous,
|
|
314
|
+
lock-free) used by both `wait_for_abtest` and the new prefetch API, removing
|
|
315
|
+
the per-context `asyncio.Lock`.
|
|
316
|
+
|
|
317
|
+
## [0.5.0] - 2026-06-19
|
|
318
|
+
|
|
319
|
+
### Fixed
|
|
320
|
+
|
|
321
|
+
- Restore version-string parity: prior `python-sdk/v0.4.0` shipped with
|
|
322
|
+
`__init__.py` still claiming 0.3.0; this release jumps to 0.5.0 to skip
|
|
323
|
+
the inconsistent state and re-establish a clean baseline.
|
|
324
|
+
(修复版本号一致性:上一版 `python-sdk/v0.4.0` 发布时 `__init__.py`
|
|
325
|
+
仍声明 0.3.0,存在历史不一致;本版直接跳到 0.5.0 重建清晰基线。)
|
|
326
|
+
|
|
327
|
+
### Added
|
|
328
|
+
|
|
329
|
+
- Auto-enable client-side `round_robin` load balancing when the channel
|
|
330
|
+
address uses the `dns:///` gRPC name resolver scheme (typically
|
|
331
|
+
`dns:///<service>.<ns>.svc.cluster.local:<port>` for K8S Headless
|
|
332
|
+
Service deployment). All other address forms (bare `host:port`,
|
|
333
|
+
`grpc://`, `grpcs://`, `passthrough:///`, `unix:`) keep grpcio's
|
|
334
|
+
default `pick_first` behavior. No SDK `Config` field changes; opt-in
|
|
335
|
+
via address string only. See `docs/usage-and-integration.md` §4.1.
|
|
336
|
+
|
|
337
|
+
## [0.4.0] - 2026-06-18
|
|
338
|
+
|
|
339
|
+
### Removed (BREAKING)
|
|
340
|
+
|
|
341
|
+
- `Config.exposure_sink`, `Config.exposure_dedup_ttl`, class `ExposureSink`,
|
|
342
|
+
class `ExposureEvent`, internal `ExposureEmitter`. The Python SDK no
|
|
343
|
+
longer emits exposure events on `get_config`. Use the upstream
|
|
344
|
+
experiment-result data report channel instead.
|
|
345
|
+
- `GetExperimentResultResponse.exposures` is retained on the proto wire
|
|
346
|
+
for backward compatibility but is never populated by the server.
|
|
347
|
+
|
|
348
|
+
### Added
|
|
349
|
+
|
|
350
|
+
- `GetExperimentResultResponse.gray_hits` (`repeated GrayReleaseHit`) —
|
|
351
|
+
populated when `display_type==EACH_EXPERIMENT_GROUP` and
|
|
352
|
+
`experiment_type ∈ {CONFIG_VERSION, ALL}`; otherwise an empty list.
|
|
353
|
+
|
|
354
|
+
## [0.3.0] - 2026-06-18
|
|
355
|
+
|
|
356
|
+
### Changed
|
|
357
|
+
|
|
358
|
+
- **Repository move.** The Python SDK now lives in the public
|
|
359
|
+
`Lightspeed-Intelligence/tipsy-ab-config-sdk` repository. Install via
|
|
360
|
+
`pip install "git+https://github.com/Lightspeed-Intelligence/tipsy-ab-config-sdk.git@python-sdk/v0.3.0#subdirectory=sdk/python"`
|
|
361
|
+
— no `GH_PAT` needed.
|
|
362
|
+
- **License → MIT.** The Python SDK package is now released under MIT,
|
|
363
|
+
aligned with the parent repository.
|
|
364
|
+
- **`pyproject.toml` URLs** (`Homepage`, `Documentation`, `Source`,
|
|
365
|
+
`Issues`, `Changelog`) all point at the new repository.
|
|
366
|
+
|
|
367
|
+
### Removed
|
|
368
|
+
|
|
369
|
+
- Dead `_proto` stubs that had never been imported by SDK production code
|
|
370
|
+
(`tipsy/config/v1/internal_pb2*`, `tipsy/abtest/v1/internal_pb2*`,
|
|
371
|
+
`tipsy/audit/v1/audit_pb2*`). The 3 backend-private protos that
|
|
372
|
+
produced them stayed in the private `tipsy-ab-config` repo; only the
|
|
373
|
+
2 SDK-facing protos (`config.proto` + `abtest.proto`) ship here.
|
|
374
|
+
|
|
375
|
+
### Migration
|
|
376
|
+
|
|
377
|
+
- Old installs pinned to
|
|
378
|
+
`git+https://${GH_PAT}@github.com/Lightspeed-Intelligence/tipsy-ab-config.git@python-sdk/v0.2.0#subdirectory=sdk/python`
|
|
379
|
+
continue to resolve via the legacy tag preserved on the ab-config repo.
|
|
380
|
+
Starting with v0.3.0, install from the new public repo (no PAT).
|
|
381
|
+
|
|
382
|
+
## [0.2.0] - 2025-11-21
|
|
383
|
+
|
|
384
|
+
### Added
|
|
385
|
+
|
|
386
|
+
- Optional `trace_id` keyword argument on `Client.get_experiment_result`,
|
|
387
|
+
`Client.new_abtest_context`, and `Client.abtest_scope`. When omitted or
|
|
388
|
+
empty the SDK generates a fresh UUID v4 (36-char with dashes) and forwards
|
|
389
|
+
it on the wire, so every request is trace-identifiable end-to-end without
|
|
390
|
+
caller changes.
|
|
391
|
+
- `AbtestContext.trace_id` attribute, set at construction and reused by every
|
|
392
|
+
per-namespace `GetExperimentResult` RPC the context issues.
|
|
393
|
+
- `AbtestMiddleware` now resolves the per-request `trace_id` from the
|
|
394
|
+
inbound `X-Trace-Id` header, falling back to `X-Request-Id`, and finally
|
|
395
|
+
to a freshly generated UUID. The resolved id is forwarded onto the
|
|
396
|
+
AbtestContext so it propagates to every downstream RPC.
|
|
397
|
+
- Background `PullAll` and `Subscribe` RPCs now generate a fresh `trace_id`
|
|
398
|
+
per call and log it at `debug` level before issuing the RPC, mirroring the
|
|
399
|
+
Go SDK pattern.
|
|
400
|
+
|
|
401
|
+
## [0.1.0] - 2026-06-16
|
|
402
|
+
|
|
403
|
+
### Added
|
|
404
|
+
|
|
405
|
+
- 首发 release of the Tipsy AB-config Python SDK.
|
|
406
|
+
- `Client` / `init` / `Config` — process-local config cache populated by a
|
|
407
|
+
startup `PullAll`, long-lived server-streaming `Subscribe`, and a
|
|
408
|
+
10-second fallback `PullAll` safety net.
|
|
409
|
+
- `Client.get_config_static` — pure cache read, no abtest, no exposure.
|
|
410
|
+
- `Client.get_config` — abtest-aware lookup with the per-request
|
|
411
|
+
`AbtestContext` (abtest hit > full release fallback); emits exposure
|
|
412
|
+
events asynchronously with a 5-minute per-process dedup window.
|
|
413
|
+
- `Client.new_abtest_context` — eagerly pre-fetches the prefetch
|
|
414
|
+
namespace; other namespaces lazy-loaded and memoised at-most-once on
|
|
415
|
+
first dynamic `get_config`.
|
|
416
|
+
- `Client.get_experiment_result` — exposes the full wire request
|
|
417
|
+
(`namespace`, `user_info`, `layer_ids`, `type`, `display`).
|
|
418
|
+
- `AbtestMiddleware` — FastAPI / Starlette middleware that binds
|
|
419
|
+
`AbtestContext` into `contextvars` per request.
|
|
420
|
+
- gRPC + HTTP transports (gRPC is the default; HTTP via `tipsy-ab-config[http]`).
|
|
421
|
+
- Python 3.10 / 3.11 / 3.12 / 3.13 CI matrix.
|
|
422
|
+
- Shipped proto bindings under `tipsy_ab_config/_proto/**` regenerated
|
|
423
|
+
with `grpcio-tools==1.66.2` (protobuf 5.x major emission; runtime
|
|
424
|
+
requires `protobuf>=5.29,<7`).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lightspeed-Intelligence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include tipsy_ab_config/py.typed
|
|
5
|
+
recursive-include tipsy_ab_config/_proto *.py
|
|
6
|
+
global-exclude __pycache__
|
|
7
|
+
global-exclude *.pyc
|
|
8
|
+
global-exclude *.pyo
|
|
9
|
+
prune .venv
|
|
10
|
+
prune .pytest_cache
|
|
11
|
+
prune tipsy_ab_config.egg-info
|
|
12
|
+
prune tests/__pycache__
|