Skip to content

Commit db842e3

Browse files
committed
add readme
1 parent 69d2a75 commit db842e3

3 files changed

Lines changed: 136 additions & 3 deletions

File tree

README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,129 @@ client.check_flag(
285285
)
286286
```
287287

288+
## DataStream
289+
290+
The DataStream functionality provides real-time updates for flags, companies, and users. It uses WebSocket connections to receive updates from the Schematic backend and evaluates feature flags locally using a WASM rules engine, reducing the number of network calls.
291+
292+
### Requirements
293+
294+
DataStream requires the `rulesengine` extra for local flag evaluation:
295+
296+
```bash
297+
pip install schematichq[rulesengine]
298+
# or
299+
poetry add schematichq -E rulesengine
300+
```
301+
302+
### Key Features
303+
304+
- **Real-Time Updates**: Automatically updates cached data when changes occur on the backend.
305+
- **Local Flag Evaluation**: Flag checks are evaluated locally via WASM, eliminating per-check network requests.
306+
- **Configurable Caching**: Supports both in-memory caching and custom cache providers (e.g. Redis).
307+
308+
### How to Enable DataStream
309+
310+
To enable DataStream, set `use_datastream=True` in your `AsyncSchematicConfig`:
311+
312+
```python
313+
import asyncio
314+
from schematic.client import AsyncSchematic, AsyncSchematicConfig, DataStreamConfig
315+
316+
async def main():
317+
config = AsyncSchematicConfig(
318+
use_datastream=True,
319+
)
320+
321+
async with AsyncSchematic("YOUR_API_KEY", config) as client:
322+
is_enabled = await client.check_flag(
323+
"some-flag-key",
324+
company={"id": "your-company-id"},
325+
user={"id": "your-user-id"},
326+
)
327+
328+
asyncio.run(main())
329+
```
330+
331+
### Configuring Cache TTL
332+
333+
You can customize the cache TTL (in milliseconds) via the `DataStreamConfig`:
334+
335+
```python
336+
config = AsyncSchematicConfig(
337+
use_datastream=True,
338+
datastream=DataStreamConfig(
339+
cache_ttl=300_000, # 5 minutes
340+
),
341+
)
342+
```
343+
344+
### Replicator Mode
345+
346+
When running the `schematic-datastream-replicator` service, configure the client to operate in Replicator Mode. In this mode, the client skips establishing its own WebSocket connection and instead relies on a shared cache populated by the external replicator service.
347+
348+
```python
349+
import asyncio
350+
from schematic.client import AsyncSchematic, AsyncSchematicConfig, DataStreamConfig
351+
352+
async def main():
353+
config = AsyncSchematicConfig(
354+
use_datastream=True,
355+
datastream=DataStreamConfig(
356+
replicator_mode=True,
357+
),
358+
)
359+
360+
async with AsyncSchematic("YOUR_API_KEY", config) as client:
361+
is_enabled = await client.check_flag(
362+
"some-flag-key",
363+
company={"id": "your-company-id"},
364+
)
365+
366+
asyncio.run(main())
367+
```
368+
369+
#### Cache TTL Configuration
370+
371+
When using Replicator Mode, you should set the SDK's cache TTL to match the replicator's cache TTL. The replicator defaults to an unlimited cache TTL (`0`). If the SDK uses a shorter TTL (the default is 24 hours), locally updated cache entries will be written back with the shorter TTL and eventually evicted from the shared cache.
372+
373+
To match the replicator's default unlimited TTL:
374+
375+
```python
376+
config = AsyncSchematicConfig(
377+
use_datastream=True,
378+
datastream=DataStreamConfig(
379+
replicator_mode=True,
380+
cache_ttl=0, # Unlimited, matching the replicator default
381+
),
382+
)
383+
```
384+
385+
#### Advanced Configuration
386+
387+
```python
388+
config = AsyncSchematicConfig(
389+
use_datastream=True,
390+
datastream=DataStreamConfig(
391+
replicator_mode=True,
392+
cache_ttl=0,
393+
replicator_health_url="http://my-replicator:8090/ready",
394+
replicator_health_check=60_000, # 60 seconds, in milliseconds
395+
),
396+
)
397+
```
398+
399+
#### Default Configuration
400+
401+
- **Replicator Health URL**: `http://localhost:8090/ready`
402+
- **Health Check Interval**: 30 seconds
403+
- **Cache TTL**: 24 hours (SDK default; should be set to match the replicator's TTL, which defaults to unlimited)
404+
405+
When running in Replicator Mode, the client will:
406+
- Skip establishing WebSocket connections
407+
- Periodically check if the replicator service is ready
408+
- Use cached data populated by the external replicator service
409+
- Fall back to direct API calls if the replicator is not available
410+
288411
## Webhook Verification
289412

290413
Schematic can send webhooks to notify your application of events. To ensure the security of these webhooks, Schematic signs each request using HMAC-SHA256. The Python SDK provides utility functions to verify these signatures.

src/schematic/cache/provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ class CacheProvider(Generic[T]):
99
"""Synchronous cache provider interface."""
1010

1111
def get(self, key: str) -> Optional[T]:
12-
pass
12+
raise NotImplementedError
1313

1414
def set(self, key: str, val: T, ttl_override: Optional[int] = None) -> None:
15-
pass
15+
raise NotImplementedError
1616

1717

1818
class AsyncCacheProvider(Generic[T]):

src/schematic/datastream/datastream_client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
from .websocket_client import ClientOptions as WSClientOptions, DatastreamWSClient
2020

2121

22+
_hints_cache: Dict[type, Dict[str, Any]] = {}
23+
24+
25+
def _get_type_hints(model_cls: type) -> Dict[str, Any]:
26+
"""Cached wrapper around typing.get_type_hints to avoid repeated introspection."""
27+
if model_cls not in _hints_cache:
28+
_hints_cache[model_cls] = typing.get_type_hints(model_cls)
29+
return _hints_cache[model_cls]
30+
31+
2232
def _coerce_nulls(data: dict, model_cls: type) -> dict:
2333
"""Convert null values to empty lists for required list fields.
2434
@@ -28,7 +38,7 @@ def _coerce_nulls(data: dict, model_cls: type) -> dict:
2838
if not hasattr(model_cls, "__annotations__"):
2939
return data
3040

31-
hints = typing.get_type_hints(model_cls)
41+
hints = _get_type_hints(model_cls)
3242
result = dict(data)
3343
for field_name, field_type in hints.items():
3444
if field_name not in result:

0 commit comments

Comments
 (0)