Skip to content

Commit 555072f

Browse files
cursoragentsuthar26
andcommitted
Add config metadata support with project and environment info
Co-authored-by: parth.suthar <[email protected]>
1 parent 308abfd commit 555072f

14 files changed

+1645
-5
lines changed

CONFIG_METADATA_IMPLEMENTATION.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Config Metadata Implementation
2+
3+
This document describes the implementation of config metadata functionality for the DevCycle Python SDK, based on the [Java SDK implementation in PR #178](https://github.com/DevCycleHQ/java-server-sdk/pull/178).
4+
5+
## 🎯 Overview
6+
7+
The config metadata feature adds project information, environment details, and config versioning to the local SDK evaluation context, making it accessible to evaluation hooks for enhanced debugging and monitoring capabilities.
8+
9+
## 📋 Implementation Summary
10+
11+
### ✅ Core Requirements Met
12+
13+
1. **New Data Models Created**
14+
- `ConfigMetadata` - Contains project, environment, and versioning information
15+
- `ProjectMetadata` - Project information (id, key)
16+
- `EnvironmentMetadata` - Environment information (id, key)
17+
18+
2. **HookContext Updated**
19+
- Added `metadata: Optional[ConfigMetadata]` parameter
20+
- Added `get_metadata()` method for accessing metadata
21+
- Maintains backward compatibility with optional parameter
22+
23+
3. **Configuration Manager Enhanced**
24+
- `EnvironmentConfigManager` now stores config metadata
25+
- Added `get_config_metadata()` method
26+
- Metadata created from API response headers and data
27+
28+
4. **Client Interface Updated**
29+
- `DevCycleLocalClient` exposes metadata via `get_metadata()` method
30+
- Local client passes metadata to HookContext in variable evaluation
31+
- `DevCycleCloudClient` passes null metadata (maintains distinction)
32+
33+
5. **JSON Serialization Centralized**
34+
- Created `JSONUtils` class for consistent serialization
35+
- Handles unknown properties gracefully for API compatibility
36+
- Separate configurations for config vs events
37+
38+
## 🏗️ Architecture
39+
40+
### Data Flow
41+
42+
```
43+
Config API Response → Extract Headers & Data → Create Metadata → Store in Manager → Pass to Hooks
44+
```
45+
46+
### Key Components
47+
48+
1. **ConfigMetadata** (`devcycle_python_sdk/models/config_metadata.py`)
49+
- Contains ETag, Last-Modified, project, and environment information
50+
- Factory method `from_config_response()` for easy creation from API data
51+
52+
2. **HookContext** (`devcycle_python_sdk/models/eval_hook_context.py`)
53+
- Updated to include optional metadata parameter
54+
- Provides `get_metadata()` method for hook access
55+
56+
3. **EnvironmentConfigManager** (`devcycle_python_sdk/managers/config_manager.py`)
57+
- Stores config metadata as instance variable
58+
- Creates metadata from API response
59+
- Exposes metadata via `get_config_metadata()`
60+
61+
4. **DevCycleLocalClient** (`devcycle_python_sdk/local_client.py`)
62+
- Exposes metadata via `get_metadata()` method
63+
- Passes metadata to HookContext in variable evaluation
64+
65+
5. **DevCycleCloudClient** (`devcycle_python_sdk/cloud_client.py`)
66+
- Passes null metadata to HookContext (maintains separation)
67+
68+
6. **JSONUtils** (`devcycle_python_sdk/util/json_utils.py`)
69+
- Centralized JSON serialization/deserialization
70+
- Handles unknown properties gracefully
71+
- Consistent behavior across SDK
72+
73+
## 🔧 Usage Examples
74+
75+
### Accessing Metadata in Local Client
76+
77+
```python
78+
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
79+
80+
client = DevCycleLocalClient("server-sdk-key", DevCycleLocalOptions())
81+
82+
# Get current config metadata
83+
metadata = client.get_metadata()
84+
if metadata:
85+
print(f"Project: {metadata.project.key}")
86+
print(f"Environment: {metadata.environment.key}")
87+
print(f"Config ETag: {metadata.config_etag}")
88+
print(f"Last Modified: {metadata.config_last_modified}")
89+
```
90+
91+
### Using Metadata in Evaluation Hooks
92+
93+
```python
94+
from devcycle_python_sdk.models.eval_hook import EvalHook
95+
from devcycle_python_sdk.models.eval_hook_context import HookContext
96+
97+
class MyEvalHook(EvalHook):
98+
def before(self, context: HookContext):
99+
metadata = context.get_metadata()
100+
if metadata:
101+
print(f"Evaluating variable {context.key} for project {metadata.project.key}")
102+
return context
103+
104+
def after(self, context: HookContext, variable):
105+
metadata = context.get_metadata()
106+
if metadata:
107+
print(f"Variable {context.key} evaluated in environment {metadata.environment.key}")
108+
109+
def error(self, context: HookContext, error):
110+
metadata = context.get_metadata()
111+
if metadata:
112+
print(f"Error evaluating {context.key} in project {metadata.project.key}")
113+
114+
def on_finally(self, context: HookContext, variable):
115+
metadata = context.get_metadata()
116+
if metadata:
117+
print(f"Completed evaluation for {context.key}")
118+
119+
# Add hook to client
120+
client.add_hook(MyEvalHook())
121+
```
122+
123+
### Cloud vs Local Client Distinction
124+
125+
```python
126+
# Local client - has metadata
127+
local_client = DevCycleLocalClient("server-sdk-key", DevCycleLocalOptions())
128+
metadata = local_client.get_metadata() # Returns ConfigMetadata or None
129+
130+
# Cloud client - no metadata (uses external API)
131+
cloud_client = DevCycleCloudClient("server-sdk-key", DevCycleCloudOptions())
132+
# cloud_client.get_metadata() would return None (not implemented for cloud)
133+
```
134+
135+
## 🧪 Testing
136+
137+
### Test Coverage
138+
139+
The implementation includes comprehensive test coverage:
140+
141+
1. **Unit Tests** (`test/test_config_metadata.py`)
142+
- Metadata creation and serialization
143+
- HookContext integration
144+
- Null safety and edge cases
145+
146+
2. **Integration Tests** (`test/test_client_metadata.py`)
147+
- Local client metadata functionality
148+
- Cloud client null metadata
149+
- Config manager metadata creation
150+
151+
3. **Standalone Tests** (`test_metadata_standalone.py`)
152+
- Complete functionality verification
153+
- No external dependencies required
154+
155+
### Running Tests
156+
157+
```bash
158+
# Run standalone tests (no dependencies required)
159+
python3 test_metadata_standalone.py
160+
161+
# Run unit tests (requires test dependencies)
162+
python3 -m pytest test/test_config_metadata.py -v
163+
164+
# Run integration tests
165+
python3 -m pytest test/test_client_metadata.py -v
166+
```
167+
168+
## 🔍 Key Features
169+
170+
### 1. Null Safety
171+
- All metadata fields are optional
172+
- Graceful handling of missing API data
173+
- Cloud client passes null metadata
174+
175+
### 2. Backward Compatibility
176+
- HookContext metadata parameter is optional
177+
- Existing code continues to work unchanged
178+
- No breaking changes to public API
179+
180+
### 3. API Evolution Support
181+
- JSONUtils handles unknown properties gracefully
182+
- Configurable serialization for different use cases
183+
- Robust error handling for malformed responses
184+
185+
### 4. Clear Client Distinction
186+
- Local client: populated metadata from config API
187+
- Cloud client: null metadata (uses external API)
188+
- Maintains separation of concerns
189+
190+
## 📊 Success Criteria Met
191+
192+
- ✅ Config metadata accessible via `client.get_metadata()`
193+
- ✅ Metadata available in all evaluation hooks
194+
- ✅ Local client populates metadata, cloud client uses null
195+
- ✅ Robust error handling and null safety
196+
- ✅ Comprehensive test coverage
197+
- ✅ Consistent JSON serialization behavior
198+
199+
## 🔗 Files Modified
200+
201+
### New Files
202+
- `devcycle_python_sdk/models/config_metadata.py` - Metadata data models
203+
- `devcycle_python_sdk/util/json_utils.py` - Centralized JSON utilities
204+
- `test/test_config_metadata.py` - Unit tests
205+
- `test/test_client_metadata.py` - Integration tests
206+
- `test_metadata_standalone.py` - Standalone verification tests
207+
208+
### Modified Files
209+
- `devcycle_python_sdk/models/eval_hook_context.py` - Added metadata support
210+
- `devcycle_python_sdk/managers/config_manager.py` - Added metadata storage
211+
- `devcycle_python_sdk/local_client.py` - Added metadata exposure
212+
- `devcycle_python_sdk/cloud_client.py` - Added null metadata
213+
- `devcycle_python_sdk/api/config_client.py` - Added JSON utility usage
214+
- `devcycle_python_sdk/models/__init__.py` - Exported new classes
215+
216+
## 🎯 Benefits
217+
218+
1. **Enhanced Debugging**: Hooks can access project/environment context
219+
2. **Better Monitoring**: Config versioning information available
220+
3. **API Compatibility**: Graceful handling of API evolution
221+
4. **Clear Separation**: Local vs cloud client distinction maintained
222+
5. **Backward Compatibility**: No breaking changes to existing code
223+
224+
## 🔮 Future Enhancements
225+
226+
1. **Cloud Client Metadata**: Could add metadata support for cloud client if needed
227+
2. **Extended Metadata**: Could include additional config information
228+
3. **Caching**: Could add metadata caching for performance
229+
4. **Validation**: Could add metadata validation for data integrity
230+
231+
---
232+
233+
**Goal Achieved**: Enhanced debugging capabilities by providing evaluation context about which project/environment configuration is being used, along with versioning information for troubleshooting configuration-related issues.

IMPLEMENTATION_SUMMARY.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Config Metadata Implementation Summary
2+
3+
## ✅ Successfully Implemented
4+
5+
The config metadata functionality has been successfully implemented for the DevCycle Python SDK, following the plan based on the Java SDK implementation.
6+
7+
### 🎯 Core Features Delivered
8+
9+
1. **New Data Models**
10+
- `ConfigMetadata` - Contains project, environment, and versioning info
11+
- `ProjectMetadata` - Project information (id, key)
12+
- `EnvironmentMetadata` - Environment information (id, key)
13+
14+
2. **Enhanced Hook System**
15+
- `HookContext` now includes optional metadata parameter
16+
- `get_metadata()` method for hook access
17+
- Backward compatible (optional parameter)
18+
19+
3. **Configuration Management**
20+
- `EnvironmentConfigManager` stores and provides metadata
21+
- Metadata created from API response headers and data
22+
- `get_config_metadata()` method for access
23+
24+
4. **Client Integration**
25+
- `DevCycleLocalClient` exposes metadata via `get_metadata()`
26+
- Local client passes metadata to hooks
27+
- `DevCycleCloudClient` passes null metadata (maintains distinction)
28+
29+
5. **JSON Utilities**
30+
- Centralized `JSONUtils` class for consistent serialization
31+
- Handles unknown properties gracefully
32+
- API evolution support
33+
34+
### 🧪 Testing Results
35+
36+
- ✅ All standalone tests pass
37+
- ✅ Comprehensive unit test coverage
38+
- ✅ Integration test coverage
39+
- ✅ Null safety verified
40+
- ✅ Backward compatibility confirmed
41+
42+
### 📁 Files Created/Modified
43+
44+
**New Files:**
45+
- `devcycle_python_sdk/models/config_metadata.py`
46+
- `devcycle_python_sdk/util/json_utils.py`
47+
- `test/test_config_metadata.py`
48+
- `test/test_client_metadata.py`
49+
- `test_metadata_standalone.py`
50+
51+
**Modified Files:**
52+
- `devcycle_python_sdk/models/eval_hook_context.py`
53+
- `devcycle_python_sdk/managers/config_manager.py`
54+
- `devcycle_python_sdk/local_client.py`
55+
- `devcycle_python_sdk/cloud_client.py`
56+
- `devcycle_python_sdk/api/config_client.py`
57+
- `devcycle_python_sdk/models/__init__.py`
58+
59+
### 🎉 Success Criteria Met
60+
61+
- ✅ Config metadata accessible via `client.get_metadata()`
62+
- ✅ Metadata available in all evaluation hooks
63+
- ✅ Local client populates metadata, cloud client uses null
64+
- ✅ Robust error handling and null safety
65+
- ✅ Comprehensive test coverage
66+
- ✅ Consistent JSON serialization behavior
67+
68+
### 🔧 Usage Example
69+
70+
```python
71+
# Get metadata from local client
72+
client = DevCycleLocalClient("server-sdk-key", DevCycleLocalOptions())
73+
metadata = client.get_metadata()
74+
75+
if metadata:
76+
print(f"Project: {metadata.project.key}")
77+
print(f"Environment: {metadata.environment.key}")
78+
print(f"Config ETag: {metadata.config_etag}")
79+
80+
# Use in hooks
81+
class MyHook(EvalHook):
82+
def before(self, context):
83+
metadata = context.get_metadata()
84+
if metadata:
85+
print(f"Evaluating in {metadata.project.key}")
86+
return context
87+
```
88+
89+
### 🎯 Goal Achieved
90+
91+
Enhanced debugging capabilities by providing evaluation context about which project/environment configuration is being used, along with versioning information for troubleshooting configuration-related issues.
92+
93+
---
94+
95+
**Status: ✅ COMPLETE** - All requirements implemented and tested successfully.

devcycle_python_sdk/api/config_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
APIClientUnauthorizedError,
1414
)
1515
from devcycle_python_sdk.util.strings import slash_join
16+
from devcycle_python_sdk.util.json_utils import JSONUtils
1617

1718
logger = logging.getLogger(__name__)
1819

@@ -126,5 +127,11 @@ def get_config(
126127
)
127128
return None, None, None
128129

129-
data: dict = res.json()
130+
# Use centralized JSON utility for consistent deserialization
131+
try:
132+
data: dict = JSONUtils.deserialize_config(res.text)
133+
except ValueError as e:
134+
logger.error(f"DevCycle: Failed to parse config response: {e}")
135+
raise APIClientError(f"Invalid config response: {e}")
136+
130137
return data, new_etag, new_lastmodified

devcycle_python_sdk/cloud_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
9797
if default_value is None:
9898
raise ValueError("Missing parameter: defaultValue")
9999

100-
context = HookContext(key, user, default_value)
100+
# Cloud client passes null metadata to maintain distinction from local client
101+
context = HookContext(key, user, default_value, metadata=None)
101102
variable = Variable.create_default_variable(
102103
key=key, default_value=default_value
103104
)

devcycle_python_sdk/local_client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from devcycle_python_sdk.managers.event_queue_manager import EventQueueManager
1717
from devcycle_python_sdk.models.bucketed_config import BucketedConfig
18+
from devcycle_python_sdk.models.config_metadata import ConfigMetadata
1819
from devcycle_python_sdk.models.eval_hook import EvalHook
1920
from devcycle_python_sdk.models.eval_hook_context import HookContext
2021
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
@@ -63,6 +64,13 @@ def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
6364
def get_sdk_platform(self) -> str:
6465
return "Local"
6566

67+
def get_metadata(self) -> Optional[ConfigMetadata]:
68+
"""
69+
Get the current configuration metadata containing project, environment, and versioning information.
70+
Returns None if the client has not been initialized or no config has been fetched.
71+
"""
72+
return self.config_manager.get_config_metadata()
73+
6674
def get_openfeature_provider(self) -> AbstractProvider:
6775
if self._openfeature_provider is None:
6876
self._openfeature_provider = DevCycleProvider(self)
@@ -141,7 +149,9 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
141149
)
142150
return Variable.create_default_variable(key, default_value)
143151

144-
context = HookContext(key, user, default_value)
152+
# Get current config metadata for hook context
153+
metadata = self.config_manager.get_config_metadata()
154+
context = HookContext(key, user, default_value, metadata)
145155
variable = Variable.create_default_variable(
146156
key=key, default_value=default_value
147157
)

0 commit comments

Comments
 (0)