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.
Files changed (57) hide show
  1. tipsy_ab_config-0.15.0/CHANGELOG.md +424 -0
  2. tipsy_ab_config-0.15.0/LICENSE +21 -0
  3. tipsy_ab_config-0.15.0/MANIFEST.in +12 -0
  4. tipsy_ab_config-0.15.0/PKG-INFO +268 -0
  5. tipsy_ab_config-0.15.0/README.md +227 -0
  6. tipsy_ab_config-0.15.0/pyproject.toml +73 -0
  7. tipsy_ab_config-0.15.0/setup.cfg +4 -0
  8. tipsy_ab_config-0.15.0/tests/test_abtest_context.py +363 -0
  9. tipsy_ab_config-0.15.0/tests/test_abtest_context_traceid.py +204 -0
  10. tipsy_ab_config-0.15.0/tests/test_auth_interceptor.py +198 -0
  11. tipsy_ab_config-0.15.0/tests/test_backoff_reset.py +97 -0
  12. tipsy_ab_config-0.15.0/tests/test_cache.py +232 -0
  13. tipsy_ab_config-0.15.0/tests/test_enrollment_alignment.py +183 -0
  14. tipsy_ab_config-0.15.0/tests/test_enrollment_log.py +688 -0
  15. tipsy_ab_config-0.15.0/tests/test_enrollment_merge.py +353 -0
  16. tipsy_ab_config-0.15.0/tests/test_fastapi.py +421 -0
  17. tipsy_ab_config-0.15.0/tests/test_fastapi_middleware_traceid.py +250 -0
  18. tipsy_ab_config-0.15.0/tests/test_get_all_configs.py +492 -0
  19. tipsy_ab_config-0.15.0/tests/test_get_config.py +337 -0
  20. tipsy_ab_config-0.15.0/tests/test_get_experiment_result_traceid.py +188 -0
  21. tipsy_ab_config-0.15.0/tests/test_grpc_target.py +523 -0
  22. tipsy_ab_config-0.15.0/tests/test_heartbeat.py +124 -0
  23. tipsy_ab_config-0.15.0/tests/test_http_transport.py +1024 -0
  24. tipsy_ab_config-0.15.0/tests/test_metrics.py +37 -0
  25. tipsy_ab_config-0.15.0/tests/test_new_abtest_context_traceid.py +199 -0
  26. tipsy_ab_config-0.15.0/tests/test_packaging.py +199 -0
  27. tipsy_ab_config-0.15.0/tests/test_prefetch.py +255 -0
  28. tipsy_ab_config-0.15.0/tests/test_pull.py +170 -0
  29. tipsy_ab_config-0.15.0/tests/test_pull_subscribe_traceid.py +242 -0
  30. tipsy_ab_config-0.15.0/tests/test_rpc_timing_log.py +258 -0
  31. tipsy_ab_config-0.15.0/tests/test_secret_key_auth.py +355 -0
  32. tipsy_ab_config-0.15.0/tests/test_service_config.py +252 -0
  33. tipsy_ab_config-0.15.0/tests/test_subscribe.py +138 -0
  34. tipsy_ab_config-0.15.0/tests/test_subscribe_backoff_reset.py +176 -0
  35. tipsy_ab_config-0.15.0/tests/test_typed_config.py +413 -0
  36. tipsy_ab_config-0.15.0/tests/test_v2_namespace.py +443 -0
  37. tipsy_ab_config-0.15.0/tests/test_version_sync.py +129 -0
  38. tipsy_ab_config-0.15.0/tipsy_ab_config/__init__.py +53 -0
  39. tipsy_ab_config-0.15.0/tipsy_ab_config/_http_transport.py +159 -0
  40. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/__init__.py +0 -0
  41. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/__init__.py +0 -0
  42. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/__init__.py +0 -0
  43. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/v1/__init__.py +0 -0
  44. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/v1/abtest_pb2.py +85 -0
  45. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/abtest/v1/abtest_pb2_grpc.py +219 -0
  46. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/__init__.py +0 -0
  47. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/v1/__init__.py +0 -0
  48. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/v1/config_pb2.py +113 -0
  49. tipsy_ab_config-0.15.0/tipsy_ab_config/_proto/tipsy/config/v1/config_pb2_grpc.py +523 -0
  50. tipsy_ab_config-0.15.0/tipsy_ab_config/abtest_context.py +318 -0
  51. tipsy_ab_config-0.15.0/tipsy_ab_config/cache.py +231 -0
  52. tipsy_ab_config-0.15.0/tipsy_ab_config/client.py +2233 -0
  53. tipsy_ab_config-0.15.0/tipsy_ab_config/exceptions.py +46 -0
  54. tipsy_ab_config-0.15.0/tipsy_ab_config/fastapi_middleware.py +176 -0
  55. tipsy_ab_config-0.15.0/tipsy_ab_config/metrics.py +96 -0
  56. tipsy_ab_config-0.15.0/tipsy_ab_config/py.typed +0 -0
  57. 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__