@@ -15,7 +15,7 @@ Python SDK for [Replane](https://replane.dev) - a dynamic configuration platform
1515- ** Context-based overrides** for feature flags, A/B testing, and gradual rollouts
1616- ** Zero dependencies** for sync client (stdlib only)
1717- ** Both sync and async** clients available
18- - ** Type-safe** with full type hints
18+ - ** Type-safe** with TypedDict support and full type hints
1919- ** Testing utilities** with in-memory client
2020
2121## Installation
@@ -41,16 +41,14 @@ with Replane(
4141 sdk_key = " rp_..." ,
4242) as client:
4343 # Get a simple config value
44- rate_limit = client.get( " rate-limit" )
44+ rate_limit = client.configs[ " rate-limit" ]
4545
4646 # Get with context for override evaluation
47- feature_enabled = client.get(
48- " new-feature" ,
49- context = {" user_id" : user.id, " plan" : user.plan},
50- )
47+ user_client = client.with_context({" user_id" : user.id, " plan" : user.plan})
48+ feature_enabled = user_client.configs[" new-feature" ]
5149
5250 # Get with fallback default
53- timeout = client.get(" request-timeout" , default = 30 )
51+ timeout = client.configs. get(" request-timeout" , 30 )
5452```
5553
5654### Asynchronous Client
@@ -64,13 +62,48 @@ async with AsyncReplane(
6462 base_url = " https://replane.example.com" ,
6563 sdk_key = " rp_..." ,
6664) as client:
67- # get() is sync since it reads from local cache
68- rate_limit = client.get( " rate-limit" )
65+ # Access configs from local cache
66+ rate_limit = client.configs[ " rate-limit" ]
6967
7068 # With context
71- enabled = client.get(" feature" , context = {" plan" : " premium" })
69+ enabled = client.with_context({" plan" : " premium" }).configs[" feature" ]
70+ ```
71+
72+ ### Type-Safe with Generated Types (Recommended)
73+
74+ Generate TypedDict types from your Replane dashboard for full type safety:
75+
76+ ``` python
77+ from replane import Replane
78+ from replane_types import Configs # Generated from Replane dashboard
79+
80+ # Use the Configs TypedDict as a type parameter
81+ with Replane[Configs](
82+ base_url = " https://cloud.replane.dev" ,
83+ sdk_key = " rp_..." ,
84+ ) as client:
85+ # Access configs with dictionary-style notation
86+ settings = client.configs[" app-settings" ]
87+
88+ # Full type safety - IDE knows the structure of settings
89+ print (settings[" maxUploadSizeMb" ])
90+ print (settings[" allowedFileTypes" ])
91+
92+ # Check if config exists
93+ if " feature-flag" in client.configs:
94+ flag = client.configs[" feature-flag" ]
95+
96+ # Safe access with default
97+ timeout = client.configs.get(" timeout" , 30 )
7298```
7399
100+ The ` .configs ` property provides:
101+
102+ - ** Dictionary-style access** with ` client.configs["config-name"] `
103+ - ** Type inference** when using generated TypedDict types
104+ - ** Override evaluation** using the default context
105+ - ** Familiar dict methods** : ` .get() ` , ` .keys() ` , ` in ` operator
106+
74107## Configuration Options
75108
76109Both clients accept the same configuration:
@@ -80,7 +113,7 @@ client = Replane(
80113 base_url = " https://replane.example.com" ,
81114 sdk_key = " rp_..." ,
82115
83- # Default context applied to all get() calls
116+ # Default context applied to all config evaluations
84117 context = {" environment" : " production" },
85118
86119 # Default values used if server is unavailable during init
@@ -119,31 +152,82 @@ context = {
119152 " is_beta_tester" : True ,
120153}
121154
122- # Overrides are evaluated locally
123- value = client.get(" feature-flag" , context = context)
155+ # Overrides are evaluated locally using with_context()
156+ value = client.with_context(context).configs[" feature-flag" ]
157+ ```
158+
159+ ### Scoped Clients with ` with_context() `
160+
161+ Create scoped clients for specific users or requests using ` with_context() ` :
162+
163+ ``` python
164+ with Replane(
165+ base_url = " https://cloud.replane.dev" ,
166+ sdk_key = " rp_..." ,
167+ ) as client:
168+ # Create a scoped client for a specific user
169+ user_client = client.with_context({
170+ " user_id" : user.id,
171+ " plan" : user.plan,
172+ })
173+
174+ # All operations use the merged context
175+ rate_limit = user_client.configs[" rate-limit" ]
176+ settings = user_client.configs[" app-settings" ]
177+
178+ # Can be chained for additional context
179+ request_client = user_client.with_context({" region" : request.region})
180+ ```
181+
182+ The original client is unaffected - scoped clients are lightweight wrappers.
183+
184+ ### Scoped Defaults with ` with_defaults() `
185+
186+ Create scoped clients with fallback values using ` with_defaults() ` :
187+
188+ ``` python
189+ with Replane(
190+ base_url = " https://cloud.replane.dev" ,
191+ sdk_key = " rp_..." ,
192+ ) as client:
193+ # Create a client with fallback defaults
194+ safe_client = client.with_defaults({
195+ " timeout" : 30 ,
196+ " max-retries" : 3 ,
197+ })
198+
199+ # Returns the default if config doesn't exist
200+ timeout = safe_client.configs[" timeout" ] # 30 if not configured
201+
202+ # Chain with with_context() for both features
203+ user_client = client.with_context({" plan" : " premium" }).with_defaults({
204+ " rate-limit" : 1000 ,
205+ })
124206```
125207
208+ Explicit defaults in ` .configs.get() ` take precedence over scoped defaults.
209+
126210### Override Examples
127211
128212** Percentage rollout** (gradual feature release):
129213
130214``` python
131215# Server config has 10% rollout based on user_id
132216# Same user always gets same result (deterministic hashing)
133- enabled = client.get( " new-checkout " , context = {" user_id" : user.id})
217+ enabled = client.with_context( {" user_id" : user.id}).configs[ " new-checkout " ]
134218```
135219
136220** Plan-based features** :
137221
138222``` python
139- max_items = client.get( " max-items " , context = {" plan" : user.plan})
223+ max_items = client.with_context( {" plan" : user.plan}).configs[ " max-items " ]
140224# Returns different values for free/pro/enterprise plans
141225```
142226
143227** Geographic targeting** :
144228
145229``` python
146- content = client.get( " homepage-banner " , context = {" country" : request.country})
230+ content = client.with_context( {" country" : request.country}).configs[ " homepage-banner " ]
147231```
148232
149233## Subscribing to Changes
@@ -190,7 +274,7 @@ from replane import (
190274)
191275
192276try :
193- value = client.get( " my-config" )
277+ value = client.configs[ " my-config" ]
194278except ConfigNotFoundError as e:
195279 print (f " Config not found: { e.config_name} " )
196280except TimeoutError as e:
@@ -214,7 +298,7 @@ client = create_test_client({
214298 " rate-limit" : 100 ,
215299})
216300
217- assert client.get( " feature-enabled" ) is True
301+ assert client.configs[ " feature-enabled" ] is True
218302
219303# With overrides
220304client = InMemoryReplaneClient()
@@ -230,8 +314,8 @@ client.set_config(
230314 }],
231315)
232316
233- assert client.get( " feature " , context = {" plan" : " free" }) is False
234- assert client.get( " feature " , context = {" plan" : " pro" }) is True
317+ assert client.with_context( {" plan" : " free" }).configs[ " feature " ] is False
318+ assert client.with_context( {" plan" : " pro" }).configs[ " feature " ] is True
235319```
236320
237321### Pytest Fixture Example
@@ -248,7 +332,7 @@ def replane_client():
248332 })
249333
250334def test_feature_flag (replane_client ):
251- flags = replane_client.get( " feature-flags" )
335+ flags = replane_client.configs[ " feature-flags" ]
252336 assert flags[" dark-mode" ] is True
253337```
254338
@@ -261,15 +345,15 @@ If you prefer not to use context managers:
261345client = Replane(base_url = " ..." , sdk_key = " ..." )
262346client.connect() # Blocks until initialized
263347try :
264- value = client.get( " config" )
348+ value = client.configs[ " config" ]
265349finally :
266350 client.close()
267351
268352# Async
269353client = AsyncReplane(base_url = " ..." , sdk_key = " ..." )
270354await client.connect()
271355try :
272- value = client.get( " config" )
356+ value = client.configs[ " config" ]
273357finally :
274358 await client.close()
275359```
@@ -304,7 +388,7 @@ def get_replane() -> AsyncReplane:
304388
305389@app.get (" /items" )
306390async def get_items (replane : AsyncReplane = Depends(get_replane)):
307- max_items = replane.get( " max-items " , context = {" plan" : " free" })
391+ max_items = replane.with_context( {" plan" : " free" }).configs[ " max-items " ]
308392 return {" max_items" : max_items}
309393```
310394
@@ -328,7 +412,7 @@ def init_replane():
328412
329413@app.route (" /items" )
330414def get_items ():
331- max_items = replane_client.get( " max-items" )
415+ max_items = replane_client.configs[ " max-items" ]
332416 return {" max_items" : max_items}
333417```
334418
0 commit comments