Coverage for src/dataknobs_fsm/resources/properties.py: 0%

1"""Properties custom resource for testing and demonstration.

2

3 This module provides a simple dictionary-based resource that can be used

4 for testing FSM resource management and as an example of custom resources.

5"""

6

7 import logging

8 from datetime import datetime

9 from typing import Any, Dict, List

10 from dataknobs_fsm.resources.base import BaseResourceProvider, ResourceStatus, ResourceHealth

11 from dataknobs_fsm.functions.base import ResourceError

12

13 logger = logging.getLogger(__name__)

14

15

16 class PropertiesHandle:

17 """Handle for a properties resource instance."""

18

19 def __init__(self, resource_name: str, owner_id: str, instance_id: str, properties: Dict[str, Any]):

20 """Initialize properties handle.

21

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 = []

36

37 def get(self, key: str, default: Any = None) -> Any:

38 """Get a property value.

39

40 Args:

41 key: Property key.

42 default: Default value if key not found.

43

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)

50

51 def set(self, key: str, value: Any) -> None:

52 """Set a property value.

53

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 })

67

68 def update(self, properties: Dict[str, Any]) -> None:

69 """Update multiple properties.

70

71 Args:

72 properties: Dictionary of properties to update.

73 """

74 for key, value in properties.items():

75 self.set(key, value)

76

77 def delete(self, key: str) -> Any:

78 """Delete a property.

79

80 Args:

81 key: Property key to delete.

82

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

97

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 })

105

106 def to_dict(self) -> Dict[str, Any]:

107 """Convert handle to dictionary representation.

108

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 }

122

123

124 class PropertiesResource(BaseResourceProvider):

125 """Resource provider that manages property dictionaries.

126

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 """

133

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.

143

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)

152

153 # Configuration

154 self.initial_properties = initial_properties or {}

155 self.max_instances = max_instances

156 self.track_history = track_history

157

158 # Instance tracking

159 self._instances: Dict[str, PropertiesHandle] = {}

160 self._instance_counter = 0

161

162 # History tracking

163 self._acquisition_history: List[Dict[str, Any]] = []

164 self._release_history: List[Dict[str, Any]] = []

165

166 # State

167 self.status = ResourceStatus.IDLE

168 logger.info(f"Initialized PropertiesResource '{name}' with max_instances={max_instances}")

169

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.

177

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).

182

183 Returns:

184 PropertiesHandle instance.

185

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 )

196

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"

201

202 # Merge properties

203 merged_properties = self.initial_properties.copy()

204 if properties:

205 merged_properties.update(properties)

206

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 }

214

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 )

222

223 # Track instance

224 self._instances[instance_id] = handle

225 self._resources.append(handle)

226

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 })

235

236 # Update status

237 self.status = ResourceStatus.ACTIVE

238 self.metrics.record_acquisition()

239

240 logger.debug(f"Acquired properties resource: {instance_id} for owner: {owner_id}")

241 return handle

242

243 def release(self, resource: Any) -> None:

244 """Release a properties resource instance.

245

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

259

260 # Remove from tracking

261 if instance_id in self._instances:

262 del self._instances[instance_id]

263

264 if handle and handle in self._resources:

265 self._resources.remove(handle)

266

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 })

277

278 self.metrics.record_release(

279 (datetime.now() - handle.created_at).total_seconds()

280 )

281

282 logger.debug(f"Released properties resource: {instance_id}")

283

284 # Update status

285 if not self._instances:

286 self.status = ResourceStatus.IDLE

287

288 def get_instance(self, instance_id: str) -> PropertiesHandle | None:

289 """Get a specific instance by ID.

290

291 Args:

292 instance_id: Instance identifier.

293

294 Returns:

295 PropertiesHandle or None if not found.

296 """

297 return self._instances.get(instance_id)

298

299 def get_all_instances(self) -> Dict[str, PropertiesHandle]:

300 """Get all active instances.

301

302 Returns:

303 Dictionary of instance_id -> PropertiesHandle.

304 """

305 return self._instances.copy()

306

307 def health_check(self) -> ResourceHealth:

308 """Check resource health.

309

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

319

320 def get_stats(self) -> Dict[str, Any]:

321 """Get resource statistics.

322

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 }

336

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 }

343

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 }

353

354 return stats

355

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)

361

362 # Clear history if needed

363 self._acquisition_history.clear()

364 self._release_history.clear()

365 self._instance_counter = 0

366

367 logger.info(f"Reset PropertiesResource '{self.name}'")

368

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 )