Skip to content

Commit 4f0b9df

Browse files
committed
chore: update python sdk
1 parent eb93fbd commit 4f0b9df

10 files changed

Lines changed: 173 additions & 165 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,6 @@ client.subscribe(on_change)
266266
```python
267267
from replane import (
268268
ReplaneError,
269-
ConfigNotFoundError,
270269
TimeoutError,
271270
AuthenticationError,
272271
NetworkError,
@@ -275,8 +274,8 @@ from replane import (
275274

276275
try:
277276
value = client.configs["my-config"]
278-
except ConfigNotFoundError as e:
279-
print(f"Config not found: {e.config_name}")
277+
except KeyError as e:
278+
print(f"Config not found: {e}")
280279
except TimeoutError as e:
281280
print(f"Timed out after {e.timeout_ms}ms")
282281
except AuthenticationError:

docs/source/configuration.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ sdk_key="rp_test_xyz789..." # Testing/staging
5959
- **Type:** `dict[str, str | int | float | bool | None]`
6060
- **Default:** `{}`
6161

62-
Default context applied to all `get()` calls. This is merged with any context passed directly to `get()`.
62+
Default context applied to all config accesses. This is merged with any context passed via `with_context()`.
6363

6464
```python
6565
# Set default context
@@ -71,11 +71,12 @@ replane = Replane(
7171
},
7272
)
7373

74-
# This call uses the default context
75-
value = replane.get("config-name")
74+
# This uses the default context
75+
value = replane.configs["config-name"]
7676

77-
# This merges with default context
78-
value = replane.get("config-name", context={"user_id": "123"})
77+
# This merges with default context using with_context()
78+
user_client = replane.with_context({"user_id": "123"})
79+
value = user_client.configs["config-name"]
7980
# Effective context: {"environment": "production", "region": "us-east", "user_id": "123"}
8081
```
8182

@@ -100,7 +101,7 @@ replane = Replane(
100101
Defaults are used in two scenarios:
101102

102103
1. During initialization if a config isn't returned by the server
103-
2. If `get()` is called before initialization completes
104+
2. If configs are accessed before initialization completes
104105

105106
#### `required`
106107

@@ -226,7 +227,7 @@ replane = Replane(base_url="...", sdk_key="...")
226227
replane.connect() # Blocks until ready
227228

228229
try:
229-
value = replane.get("config")
230+
value = replane.configs["config"]
230231
finally:
231232
replane.close()
232233
```
@@ -250,7 +251,7 @@ replane = AsyncReplane(base_url="...", sdk_key="...")
250251
await replane.connect()
251252

252253
try:
253-
value = replane.get("config")
254+
value = replane.configs["config"]
254255
finally:
255256
await replane.close()
256257
```

docs/source/errors.md

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The Replane SDK uses a hierarchy of exceptions to help you handle errors appropr
66

77
```
88
ReplaneError (base class)
9-
├── ConfigNotFoundError
9+
├── ConfigNotFoundError (used for required configs at init time)
1010
├── TimeoutError
1111
├── AuthenticationError
1212
├── NetworkError
@@ -15,13 +15,15 @@ ReplaneError (base class)
1515
└── MissingDependencyError
1616
```
1717

18+
Note: Accessing a missing config via `client.configs["name"]` raises a standard `KeyError`, not `ConfigNotFoundError`. Use `client.configs.get("name", default)` to avoid exceptions.
19+
1820
## Error Codes
1921

2022
Each `ReplaneError` has a `code` attribute from the `ErrorCode` enum:
2123

2224
| Code | Description |
2325
| -------------------- | --------------------------------------- |
24-
| `not_found` | Config doesn't exist |
26+
| `not_found` | Config doesn't exist (required configs) |
2527
| `timeout` | Operation timed out |
2628
| `network_error` | Network request failed |
2729
| `auth_error` | Authentication failed (invalid SDK key) |
@@ -41,7 +43,6 @@ Each `ReplaneError` has a `code` attribute from the `ErrorCode` enum:
4143
from replane import (
4244
Replane,
4345
ReplaneError,
44-
ConfigNotFoundError,
4546
TimeoutError,
4647
AuthenticationError,
4748
)
@@ -51,9 +52,9 @@ try:
5152
base_url="https://replane.example.com",
5253
sdk_key="rp_...",
5354
) as replane:
54-
value = replane.get("my-config")
55-
except ConfigNotFoundError as e:
56-
print(f"Config '{e.config_name}' not found")
55+
value = replane.configs["my-config"]
56+
except KeyError as e:
57+
print(f"Config not found: {e}")
5758
except TimeoutError as e:
5859
print(f"Timed out after {e.timeout_ms}ms")
5960
except AuthenticationError:
@@ -68,12 +69,9 @@ except ReplaneError as e:
6869
from replane import ReplaneError, ErrorCode
6970

7071
try:
71-
value = replane.get("config")
72+
replane.connect()
7273
except ReplaneError as e:
7374
match e.code:
74-
case ErrorCode.NOT_FOUND:
75-
# Handle missing config
76-
value = default_value
7775
case ErrorCode.TIMEOUT:
7876
# Maybe retry
7977
pass
@@ -87,38 +85,56 @@ except ReplaneError as e:
8785

8886
## Specific Exceptions
8987

90-
### ConfigNotFoundError
88+
### KeyError (Missing Config)
9189

92-
Raised when requesting a config that doesn't exist.
90+
Accessing a missing config via bracket notation raises a standard `KeyError`:
9391

9492
```python
95-
from replane import ConfigNotFoundError
96-
9793
try:
98-
value = replane.get("nonexistent-config")
99-
except ConfigNotFoundError as e:
100-
print(f"Config not found: {e.config_name}")
101-
# Use a default value instead
94+
value = replane.configs["nonexistent-config"]
95+
except KeyError as e:
96+
print(f"Config not found: {e}")
10297
value = "default"
10398
```
10499

105-
**Attributes:**
106-
107-
- `config_name: str` - Name of the missing config
108-
109-
**Prevention:** Use `default` parameter or `defaults` option:
100+
**Prevention:** Use `.get()` method or `defaults` option:
110101

111102
```python
112-
# With default
113-
value = replane.get("config", default="fallback")
103+
# With get() method
104+
value = replane.configs.get("config", "fallback")
114105

115106
# With defaults during init
116107
replane = Replane(
117108
...,
118109
defaults={"config": "fallback"},
119110
)
111+
112+
# With with_defaults()
113+
safe_client = replane.with_defaults({"config": "fallback"})
114+
value = safe_client.configs["config"] # Returns "fallback" if not configured
115+
```
116+
117+
### ConfigNotFoundError
118+
119+
Raised when required configs are missing during initialization.
120+
121+
```python
122+
from replane import Replane, ConfigNotFoundError
123+
124+
try:
125+
with Replane(
126+
...,
127+
required=["critical-config-1", "critical-config-2"],
128+
) as replane:
129+
pass
130+
except ConfigNotFoundError as e:
131+
print(f"Missing required configs: {e}")
120132
```
121133

134+
**Attributes:**
135+
136+
- `config_name: str` - Name or description of missing config(s)
137+
122138
### TimeoutError
123139

124140
Raised when an operation exceeds its timeout.
@@ -199,7 +215,7 @@ replane.connect()
199215
replane.close()
200216

201217
try:
202-
replane.get("config") # Raises ClientClosedError
218+
_ = replane.configs["config"] # Raises ClientClosedError
203219
except ClientClosedError:
204220
print("Client was already closed")
205221
```
@@ -215,10 +231,10 @@ replane = Replane(...)
215231
replane.connect(wait=False) # Don't wait
216232

217233
try:
218-
replane.get("config") # May raise if not ready
234+
_ = replane.configs["config"] # May raise if not ready
219235
except NotInitializedError:
220236
replane.wait_for_init() # Wait then retry
221-
value = replane.get("config")
237+
value = replane.configs["config"]
222238
```
223239

224240
### MissingDependencyError
@@ -259,22 +275,22 @@ except ReplaneError as e:
259275
2. **Use defaults** for resilience against missing configs
260276
3. **Log errors** with their codes for debugging
261277
4. **Don't catch and ignore** - at minimum, log the error
262-
5. **Use `default` parameter** instead of catching `ConfigNotFoundError` when appropriate
278+
5. **Use `.get()` method** instead of catching `KeyError` when appropriate
263279

264280
```python
265281
# Good: specific handling
266282
try:
267-
value = replane.get("critical-config")
268-
except ConfigNotFoundError:
283+
value = replane.configs["critical-config"]
284+
except KeyError:
269285
logger.error("Critical config missing!")
270286
raise # Re-raise for critical configs
271287

272288
# Good: graceful fallback
273-
value = replane.get("optional-config", default="safe-default")
289+
value = replane.configs.get("optional-config", "safe-default")
274290

275291
# Bad: silently ignoring
276292
try:
277-
value = replane.get("config")
278-
except ReplaneError:
293+
value = replane.configs["config"]
294+
except KeyError:
279295
pass # Don't do this!
280296
```

docs/source/frameworks.md

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_replane() -> AsyncReplane:
3737

3838
@app.get("/items")
3939
async def get_items(replane: AsyncReplane = Depends(get_replane)):
40-
max_items = replane.get("max-items-per-page")
40+
max_items = replane.configs["max-items-per-page"]
4141
return {"max_items": max_items}
4242
```
4343

@@ -52,14 +52,14 @@ async def get_features(
5252
replane: AsyncReplane = Depends(get_replane),
5353
):
5454
# Build context from request/user
55-
context = {
55+
user_client = replane.with_context({
5656
"user_id": request.state.user.id,
5757
"plan": request.state.user.plan,
58-
}
58+
})
5959

6060
return {
61-
"dark_mode": replane.get("dark-mode-enabled", context=context),
62-
"beta_features": replane.get("beta-features", context=context),
61+
"dark_mode": user_client.configs["dark-mode-enabled"],
62+
"beta_features": user_client.configs["beta-features"],
6363
}
6464
```
6565

@@ -69,30 +69,22 @@ Create a dependency that automatically includes user context:
6969

7070
```python
7171
from fastapi import Request, Depends
72-
73-
class ReplaneWithContext:
74-
def __init__(self, client: AsyncReplane, context: dict):
75-
self._client = client
76-
self._context = context
77-
78-
def get(self, name: str, **kwargs):
79-
ctx = {**self._context, **kwargs.get("context", {})}
80-
return self._client.get(name, context=ctx, **{k: v for k, v in kwargs.items() if k != "context"})
72+
from replane._async import ContextualAsyncReplane
8173

8274
def get_replane_with_context(
8375
request: Request,
8476
replane: AsyncReplane = Depends(get_replane),
85-
) -> ReplaneWithContext:
77+
) -> ContextualAsyncReplane:
8678
context = {}
8779
if hasattr(request.state, "user"):
8880
context["user_id"] = request.state.user.id
8981
context["plan"] = request.state.user.plan
90-
return ReplaneWithContext(replane, context)
82+
return replane.with_context(context)
9183

9284
@app.get("/dashboard")
93-
async def dashboard(config: ReplaneWithContext = Depends(get_replane_with_context)):
85+
async def dashboard(config: ContextualAsyncReplane = Depends(get_replane_with_context)):
9486
# Context is automatically included
95-
show_analytics = config.get("show-analytics")
87+
show_analytics = config.configs["show-analytics"]
9688
return {"show_analytics": show_analytics}
9789
```
9890

@@ -124,7 +116,7 @@ def get_replane() -> Replane:
124116
@app.route("/items")
125117
def get_items():
126118
replane = get_replane()
127-
max_items = replane.get("max-items-per-page")
119+
max_items = replane.configs["max-items-per-page"]
128120
return {"max_items": max_items}
129121
```
130122

@@ -155,7 +147,7 @@ def create_app():
155147
@app.route("/features")
156148
def features():
157149
replane = current_app.replane
158-
return {"enabled": replane.get("feature-enabled")}
150+
return {"enabled": replane.configs["feature-enabled"]}
159151
```
160152

161153
### Flask Extension Pattern
@@ -201,7 +193,7 @@ def create_app():
201193

202194
@app.route("/")
203195
def index():
204-
return {"feature": replane.client.get("feature")}
196+
return {"feature": replane.client.configs["feature"]}
205197
```
206198

207199
## Django
@@ -237,9 +229,13 @@ from .replane_client import get_replane
237229

238230
def features_view(request):
239231
replane = get_replane()
240-
context = {"user_id": str(request.user.id)} if request.user.is_authenticated else {}
232+
if request.user.is_authenticated:
233+
user_client = replane.with_context({"user_id": str(request.user.id)})
234+
feature_enabled = user_client.configs["feature"]
235+
else:
236+
feature_enabled = replane.configs["feature"]
241237
return JsonResponse({
242-
"feature_enabled": replane.get("feature", context=context),
238+
"feature_enabled": feature_enabled,
243239
})
244240
```
245241

@@ -269,7 +265,7 @@ from .replane_client import get_replane
269265
async def features_view(request):
270266
replane = await get_replane()
271267
return JsonResponse({
272-
"feature_enabled": replane.get("feature"),
268+
"feature_enabled": replane.configs["feature"],
273269
})
274270
```
275271

@@ -315,7 +311,7 @@ async def shutdown():
315311
await _replane.close()
316312

317313
async def homepage(request):
318-
feature = _replane.get("feature")
314+
feature = _replane.configs["feature"]
319315
return JSONResponse({"feature": feature})
320316

321317
app = Starlette(
@@ -353,7 +349,7 @@ async def create_app():
353349
async def handler(request):
354350
replane = request.app["replane"]
355351
return web.json_response({
356-
"feature": replane.get("feature"),
352+
"feature": replane.configs["feature"],
357353
})
358354

359355
if __name__ == "__main__":

0 commit comments

Comments
 (0)