Coverage for src/dataknobs_fsm/resources/properties.py: 100%
113 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:51 -0600
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:51 -0600
1"""Properties custom resource for testing and demonstration.
3This module provides a simple dictionary-based resource that can be used
4for testing FSM resource management and as an example of custom resources.
5"""
7import logging
8from datetime import datetime
9from typing import Any, Dict, List
10from dataknobs_fsm.resources.base import BaseResourceProvider, ResourceStatus, ResourceHealth
11from dataknobs_fsm.functions.base import ResourceError
13logger = logging.getLogger(__name__)
16class PropertiesHandle:
17 """Handle for a properties resource instance."""
19 def __init__(self, resource_name: str, owner_id: str, instance_id: str, properties: Dict[str, Any]):
20 """Initialize properties handle.
22 Args:
23 resource_name: Name of the resource.
24 owner_id: ID of the owner.
25 instance_id: Unique instance identifier.
26 properties: Initial properties dictionary.
27 """
28 self.resource_name = resource_name
29 self.owner_id = owner_id
30 self.instance_id = instance_id
31 self.properties = properties.copy() # Create a copy to avoid external mutations
32 self.created_at = datetime.now()
33 self.accessed_at = datetime.now()
34 self.access_count = 0
35 self.modifications = []
37 def get(self, key: str, default: Any = None) -> Any:
38 """Get a property value.
40 Args:
41 key: Property key.
42 default: Default value if key not found.
44 Returns:
45 Property value or default.
46 """
47 self.accessed_at = datetime.now()
48 self.access_count += 1
49 return self.properties.get(key, default)
51 def set(self, key: str, value: Any) -> None:
52 """Set a property value.
54 Args:
55 key: Property key.
56 value: Property value.
57 """
58 old_value = self.properties.get(key)
59 self.properties[key] = value
60 self.accessed_at = datetime.now()
61 self.modifications.append({
62 'timestamp': datetime.now(),
63 'key': key,
64 'old_value': old_value,
65 'new_value': value
66 })
68 def update(self, properties: Dict[str, Any]) -> None:
69 """Update multiple properties.
71 Args:
72 properties: Dictionary of properties to update.
73 """
74 for key, value in properties.items():
75 self.set(key, value)
77 def delete(self, key: str) -> Any:
78 """Delete a property.
80 Args:
81 key: Property key to delete.
83 Returns:
84 The deleted value or None.
85 """
86 if key in self.properties:
87 value = self.properties.pop(key)
88 self.modifications.append({
89 'timestamp': datetime.now(),
90 'key': key,
91 'old_value': value,
92 'new_value': None,
93 'operation': 'delete'
94 })
95 return value
96 return None
98 def clear(self) -> None:
99 """Clear all properties."""
100 self.properties.clear()
101 self.modifications.append({
102 'timestamp': datetime.now(),
103 'operation': 'clear'
104 })
106 def to_dict(self) -> Dict[str, Any]:
107 """Convert handle to dictionary representation.
109 Returns:
110 Dictionary with handle information.
111 """
112 return {
113 'resource_name': self.resource_name,
114 'owner_id': self.owner_id,
115 'instance_id': self.instance_id,
116 'properties': self.properties.copy(),
117 'created_at': self.created_at.isoformat(),
118 'accessed_at': self.accessed_at.isoformat(),
119 'access_count': self.access_count,
120 'modification_count': len(self.modifications)
121 }
124class PropertiesResource(BaseResourceProvider):
125 """Resource provider that manages property dictionaries.
127 This resource is useful for:
128 - Testing resource management in FSMs
129 - Storing configuration or state data
130 - Demonstrating custom resource implementation
131 - Tracking resource lifecycle and access patterns
132 """
134 def __init__(
135 self,
136 name: str,
137 initial_properties: Dict[str, Any] | None = None,
138 max_instances: int = 10,
139 track_history: bool = True,
140 **config
141 ):
142 """Initialize properties resource.
144 Args:
145 name: Resource name.
146 initial_properties: Initial properties for all instances.
147 max_instances: Maximum number of concurrent instances.
148 track_history: Whether to track acquisition/release history.
149 **config: Additional configuration.
150 """
151 super().__init__(name, config)
153 # Configuration
154 self.initial_properties = initial_properties or {}
155 self.max_instances = max_instances
156 self.track_history = track_history
158 # Instance tracking
159 self._instances: Dict[str, PropertiesHandle] = {}
160 self._instance_counter = 0
162 # History tracking
163 self._acquisition_history: List[Dict[str, Any]] = []
164 self._release_history: List[Dict[str, Any]] = []
166 # State
167 self.status = ResourceStatus.IDLE
168 logger.info(f"Initialized PropertiesResource '{name}' with max_instances={max_instances}")
170 def acquire(
171 self,
172 owner_id: str | None = None,
173 properties: Dict[str, Any] | None = None,
174 **kwargs
175 ) -> PropertiesHandle:
176 """Acquire a properties resource instance.
178 Args:
179 owner_id: ID of the owner (e.g., state name).
180 properties: Additional properties to merge with initial properties.
181 **kwargs: Additional parameters (ignored).
183 Returns:
184 PropertiesHandle instance.
186 Raises:
187 ResourceError: If maximum instances exceeded.
188 """
189 # Check instance limit
190 if len(self._instances) >= self.max_instances:
191 raise ResourceError(
192 f"Maximum instances ({self.max_instances}) exceeded",
193 resource_name=self.name,
194 operation="acquire"
195 )
197 # Generate instance ID
198 self._instance_counter += 1
199 instance_id = f"{self.name}_instance_{self._instance_counter}"
200 owner_id = owner_id or "unknown"
202 # Merge properties
203 merged_properties = self.initial_properties.copy()
204 if properties:
205 merged_properties.update(properties)
207 # Add metadata
208 merged_properties['_metadata'] = {
209 'resource_name': self.name,
210 'instance_id': instance_id,
211 'owner_id': owner_id,
212 'acquired_at': datetime.now().isoformat()
213 }
215 # Create handle
216 handle = PropertiesHandle(
217 resource_name=self.name,
218 owner_id=owner_id,
219 instance_id=instance_id,
220 properties=merged_properties
221 )
223 # Track instance
224 self._instances[instance_id] = handle
225 self._resources.append(handle)
227 # Track history
228 if self.track_history:
229 self._acquisition_history.append({
230 'timestamp': datetime.now(),
231 'instance_id': instance_id,
232 'owner_id': owner_id,
233 'properties': merged_properties.copy()
234 })
236 # Update status
237 self.status = ResourceStatus.ACTIVE
238 self.metrics.record_acquisition()
240 logger.debug(f"Acquired properties resource: {instance_id} for owner: {owner_id}")
241 return handle
243 def release(self, resource: Any) -> None:
244 """Release a properties resource instance.
246 Args:
247 resource: PropertiesHandle or instance_id to release.
248 """
249 # Handle different input types
250 if isinstance(resource, PropertiesHandle):
251 instance_id = resource.instance_id
252 handle = resource
253 elif isinstance(resource, str):
254 instance_id = resource
255 handle = self._instances.get(instance_id)
256 else:
257 logger.warning(f"Unknown resource type for release: {type(resource)}")
258 return
260 # Remove from tracking
261 if instance_id in self._instances:
262 del self._instances[instance_id]
264 if handle and handle in self._resources:
265 self._resources.remove(handle)
267 # Track history
268 if self.track_history:
269 self._release_history.append({
270 'timestamp': datetime.now(),
271 'instance_id': instance_id,
272 'owner_id': handle.owner_id,
273 'final_properties': handle.properties.copy(),
274 'access_count': handle.access_count,
275 'modification_count': len(handle.modifications)
276 })
278 self.metrics.record_release(
279 (datetime.now() - handle.created_at).total_seconds()
280 )
282 logger.debug(f"Released properties resource: {instance_id}")
284 # Update status
285 if not self._instances:
286 self.status = ResourceStatus.IDLE
288 def get_instance(self, instance_id: str) -> PropertiesHandle | None:
289 """Get a specific instance by ID.
291 Args:
292 instance_id: Instance identifier.
294 Returns:
295 PropertiesHandle or None if not found.
296 """
297 return self._instances.get(instance_id)
299 def get_all_instances(self) -> Dict[str, PropertiesHandle]:
300 """Get all active instances.
302 Returns:
303 Dictionary of instance_id -> PropertiesHandle.
304 """
305 return self._instances.copy()
307 def health_check(self) -> ResourceHealth:
308 """Check resource health.
310 Returns:
311 Resource health status.
312 """
313 if len(self._instances) >= self.max_instances:
314 return ResourceHealth.UNHEALTHY
315 elif len(self._instances) >= self.max_instances * 0.8:
316 return ResourceHealth.DEGRADED
317 else:
318 return ResourceHealth.HEALTHY
320 def get_stats(self) -> Dict[str, Any]:
321 """Get resource statistics.
323 Returns:
324 Dictionary with resource statistics.
325 """
326 stats = {
327 'name': self.name,
328 'status': self.status.value,
329 'health': self.health_check().value,
330 'active_instances': len(self._instances),
331 'max_instances': self.max_instances,
332 'total_acquisitions': len(self._acquisition_history),
333 'total_releases': len(self._release_history),
334 'instance_ids': list(self._instances.keys())
335 }
337 # Add instance details
338 if self._instances:
339 stats['instances'] = {
340 instance_id: handle.to_dict()
341 for instance_id, handle in self._instances.items()
342 }
344 # Add metrics
345 metrics = self.get_metrics()
346 stats['metrics'] = {
347 'total_acquisitions': metrics.total_acquisitions,
348 'active_connections': metrics.active_connections,
349 'failed_acquisitions': metrics.failed_acquisitions,
350 'average_hold_time': metrics.average_hold_time,
351 'average_acquisition_time': metrics.average_acquisition_time
352 }
354 return stats
356 def reset(self) -> None:
357 """Reset the resource, releasing all instances."""
358 # Release all instances
359 for instance_id in list(self._instances.keys()):
360 self.release(instance_id)
362 # Clear history if needed
363 self._acquisition_history.clear()
364 self._release_history.clear()
365 self._instance_counter = 0
367 logger.info(f"Reset PropertiesResource '{self.name}'")
369 def __repr__(self) -> str:
370 """String representation."""
371 return (
372 f"PropertiesResource(name='{self.name}', "
373 f"instances={len(self._instances)}/{self.max_instances}, "
374 f"status={self.status.value})"
375 )