kailash 0.4.2__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +1 -1
- kailash/client/__init__.py +12 -0
- kailash/client/enhanced_client.py +306 -0
- kailash/core/actors/__init__.py +16 -0
- kailash/core/actors/connection_actor.py +566 -0
- kailash/core/actors/supervisor.py +364 -0
- kailash/edge/__init__.py +16 -0
- kailash/edge/compliance.py +834 -0
- kailash/edge/discovery.py +659 -0
- kailash/edge/location.py +582 -0
- kailash/gateway/__init__.py +33 -0
- kailash/gateway/api.py +289 -0
- kailash/gateway/enhanced_gateway.py +357 -0
- kailash/gateway/resource_resolver.py +217 -0
- kailash/gateway/security.py +227 -0
- kailash/middleware/auth/models.py +2 -2
- kailash/middleware/database/base_models.py +1 -7
- kailash/middleware/database/repositories.py +3 -1
- kailash/middleware/gateway/__init__.py +22 -0
- kailash/middleware/gateway/checkpoint_manager.py +398 -0
- kailash/middleware/gateway/deduplicator.py +382 -0
- kailash/middleware/gateway/durable_gateway.py +417 -0
- kailash/middleware/gateway/durable_request.py +498 -0
- kailash/middleware/gateway/event_store.py +459 -0
- kailash/nodes/admin/audit_log.py +364 -6
- kailash/nodes/admin/permission_check.py +817 -33
- kailash/nodes/admin/role_management.py +1242 -108
- kailash/nodes/admin/schema_manager.py +438 -0
- kailash/nodes/admin/user_management.py +1209 -681
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- kailash/nodes/code/__init__.py +8 -1
- kailash/nodes/code/async_python.py +1035 -0
- kailash/nodes/code/python.py +1 -0
- kailash/nodes/data/async_sql.py +12 -25
- kailash/nodes/data/sql.py +20 -11
- kailash/nodes/data/workflow_connection_pool.py +643 -0
- kailash/nodes/rag/__init__.py +1 -4
- kailash/resources/__init__.py +40 -0
- kailash/resources/factory.py +533 -0
- kailash/resources/health.py +319 -0
- kailash/resources/reference.py +288 -0
- kailash/resources/registry.py +392 -0
- kailash/runtime/async_local.py +711 -302
- kailash/testing/__init__.py +34 -0
- kailash/testing/async_test_case.py +353 -0
- kailash/testing/async_utils.py +345 -0
- kailash/testing/fixtures.py +458 -0
- kailash/testing/mock_registry.py +495 -0
- kailash/utils/resource_manager.py +420 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +111 -41
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,498 @@
|
|
1
|
+
"""Durable request implementation with state machine and checkpointing.
|
2
|
+
|
3
|
+
This module provides request durability through:
|
4
|
+
- State machine for request lifecycle
|
5
|
+
- Automatic checkpointing at key points
|
6
|
+
- Execution journal for audit trail
|
7
|
+
- Resumable execution after failures
|
8
|
+
"""
|
9
|
+
|
10
|
+
import asyncio
|
11
|
+
import datetime as dt
|
12
|
+
import json
|
13
|
+
import logging
|
14
|
+
import time
|
15
|
+
import uuid
|
16
|
+
from dataclasses import dataclass, field
|
17
|
+
from datetime import datetime
|
18
|
+
from enum import Enum
|
19
|
+
|
20
|
+
# Import will be added when checkpoint_manager is implemented
|
21
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Optional
|
22
|
+
|
23
|
+
from kailash.runtime import LocalRuntime
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from .checkpoint_manager import CheckpointManager
|
27
|
+
|
28
|
+
from kailash.sdk_exceptions import NodeExecutionError
|
29
|
+
from kailash.workflow import Workflow, WorkflowBuilder
|
30
|
+
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
|
34
|
+
class RequestState(Enum):
|
35
|
+
"""Request lifecycle states."""
|
36
|
+
|
37
|
+
INITIALIZED = "initialized"
|
38
|
+
VALIDATED = "validated"
|
39
|
+
WORKFLOW_CREATED = "workflow_created"
|
40
|
+
EXECUTING = "executing"
|
41
|
+
CHECKPOINTED = "checkpointed"
|
42
|
+
COMPLETED = "completed"
|
43
|
+
FAILED = "failed"
|
44
|
+
CANCELLED = "cancelled"
|
45
|
+
RESUMING = "resuming"
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class RequestMetadata:
|
50
|
+
"""Metadata for durable request."""
|
51
|
+
|
52
|
+
request_id: str
|
53
|
+
method: str
|
54
|
+
path: str
|
55
|
+
headers: Dict[str, str]
|
56
|
+
query_params: Dict[str, str]
|
57
|
+
body: Optional[Dict[str, Any]]
|
58
|
+
client_ip: str
|
59
|
+
user_id: Optional[str]
|
60
|
+
tenant_id: Optional[str]
|
61
|
+
idempotency_key: Optional[str]
|
62
|
+
created_at: datetime
|
63
|
+
updated_at: datetime
|
64
|
+
|
65
|
+
|
66
|
+
@dataclass
|
67
|
+
class Checkpoint:
|
68
|
+
"""Checkpoint data structure."""
|
69
|
+
|
70
|
+
checkpoint_id: str
|
71
|
+
request_id: str
|
72
|
+
sequence: int
|
73
|
+
name: str
|
74
|
+
state: RequestState
|
75
|
+
data: Dict[str, Any]
|
76
|
+
workflow_state: Optional[Dict[str, Any]]
|
77
|
+
created_at: datetime
|
78
|
+
size_bytes: int
|
79
|
+
|
80
|
+
def to_dict(self) -> Dict[str, Any]:
|
81
|
+
"""Convert to dictionary for storage."""
|
82
|
+
return {
|
83
|
+
"checkpoint_id": self.checkpoint_id,
|
84
|
+
"request_id": self.request_id,
|
85
|
+
"sequence": self.sequence,
|
86
|
+
"name": self.name,
|
87
|
+
"state": self.state.value,
|
88
|
+
"data": self.data,
|
89
|
+
"workflow_state": self.workflow_state,
|
90
|
+
"created_at": self.created_at.isoformat(),
|
91
|
+
"size_bytes": self.size_bytes,
|
92
|
+
}
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Checkpoint":
|
96
|
+
"""Create from dictionary."""
|
97
|
+
return cls(
|
98
|
+
checkpoint_id=data["checkpoint_id"],
|
99
|
+
request_id=data["request_id"],
|
100
|
+
sequence=data["sequence"],
|
101
|
+
name=data["name"],
|
102
|
+
state=RequestState(data["state"]),
|
103
|
+
data=data["data"],
|
104
|
+
workflow_state=data.get("workflow_state"),
|
105
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
106
|
+
size_bytes=data["size_bytes"],
|
107
|
+
)
|
108
|
+
|
109
|
+
|
110
|
+
@dataclass
|
111
|
+
class ExecutionJournal:
|
112
|
+
"""Journal of all execution events."""
|
113
|
+
|
114
|
+
request_id: str
|
115
|
+
events: List[Dict[str, Any]] = field(default_factory=list)
|
116
|
+
|
117
|
+
async def record(self, event_type: str, data: Dict[str, Any]):
|
118
|
+
"""Record an execution event."""
|
119
|
+
event = {
|
120
|
+
"timestamp": datetime.now(dt.UTC).isoformat(),
|
121
|
+
"type": event_type,
|
122
|
+
"data": data,
|
123
|
+
"sequence": len(self.events),
|
124
|
+
}
|
125
|
+
self.events.append(event)
|
126
|
+
logger.debug(f"Recorded event {event_type} for request {self.request_id}")
|
127
|
+
|
128
|
+
def get_events(self, event_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
129
|
+
"""Get events, optionally filtered by type."""
|
130
|
+
if event_type:
|
131
|
+
return [e for e in self.events if e["type"] == event_type]
|
132
|
+
return self.events
|
133
|
+
|
134
|
+
|
135
|
+
class DurableRequest:
|
136
|
+
"""Durable request with automatic checkpointing and resumability."""
|
137
|
+
|
138
|
+
def __init__(
|
139
|
+
self,
|
140
|
+
request_id: Optional[str] = None,
|
141
|
+
metadata: Optional[RequestMetadata] = None,
|
142
|
+
checkpoint_manager: Optional["CheckpointManager"] = None,
|
143
|
+
):
|
144
|
+
"""Initialize durable request."""
|
145
|
+
self.id = request_id or f"req_{uuid.uuid4().hex[:12]}"
|
146
|
+
self.metadata = metadata or self._create_default_metadata()
|
147
|
+
self.state = RequestState.INITIALIZED
|
148
|
+
self.checkpoints: List[Checkpoint] = []
|
149
|
+
self.journal = ExecutionJournal(self.id)
|
150
|
+
self.checkpoint_manager = checkpoint_manager
|
151
|
+
|
152
|
+
# Execution state
|
153
|
+
self.workflow: Optional[Workflow] = None
|
154
|
+
self.workflow_id: Optional[str] = None
|
155
|
+
self.runtime: Optional[LocalRuntime] = None
|
156
|
+
self.result: Optional[Dict[str, Any]] = None
|
157
|
+
self.error: Optional[Exception] = None
|
158
|
+
|
159
|
+
# Timing
|
160
|
+
self.start_time: Optional[float] = None
|
161
|
+
self.end_time: Optional[float] = None
|
162
|
+
self.checkpoint_count = 0
|
163
|
+
|
164
|
+
# Cancellation support
|
165
|
+
self._cancel_event = asyncio.Event()
|
166
|
+
|
167
|
+
def _create_default_metadata(self) -> RequestMetadata:
|
168
|
+
"""Create default metadata."""
|
169
|
+
now = datetime.now(dt.UTC)
|
170
|
+
return RequestMetadata(
|
171
|
+
request_id=self.id,
|
172
|
+
method="POST",
|
173
|
+
path="/api/workflow",
|
174
|
+
headers={},
|
175
|
+
query_params={},
|
176
|
+
body=None,
|
177
|
+
client_ip="0.0.0.0",
|
178
|
+
user_id=None,
|
179
|
+
tenant_id=None,
|
180
|
+
idempotency_key=None,
|
181
|
+
created_at=now,
|
182
|
+
updated_at=now,
|
183
|
+
)
|
184
|
+
|
185
|
+
async def execute(self) -> Dict[str, Any]:
|
186
|
+
"""Execute request with automatic checkpointing."""
|
187
|
+
try:
|
188
|
+
self.start_time = time.time()
|
189
|
+
await self.journal.record(
|
190
|
+
"request_started",
|
191
|
+
{
|
192
|
+
"request_id": self.id,
|
193
|
+
"state": self.state.value,
|
194
|
+
},
|
195
|
+
)
|
196
|
+
|
197
|
+
# Validate request
|
198
|
+
await self._validate_request()
|
199
|
+
|
200
|
+
# Create workflow
|
201
|
+
await self._create_workflow()
|
202
|
+
|
203
|
+
# Execute with checkpoints
|
204
|
+
result = await self._execute_workflow()
|
205
|
+
|
206
|
+
# Mark complete
|
207
|
+
self.state = RequestState.COMPLETED
|
208
|
+
self.end_time = time.time()
|
209
|
+
self.result = result
|
210
|
+
|
211
|
+
await self.journal.record(
|
212
|
+
"request_completed",
|
213
|
+
{
|
214
|
+
"request_id": self.id,
|
215
|
+
"duration_ms": (self.end_time - self.start_time) * 1000,
|
216
|
+
"checkpoint_count": self.checkpoint_count,
|
217
|
+
},
|
218
|
+
)
|
219
|
+
|
220
|
+
return {
|
221
|
+
"request_id": self.id,
|
222
|
+
"status": "completed",
|
223
|
+
"result": result,
|
224
|
+
"duration_ms": (self.end_time - self.start_time) * 1000,
|
225
|
+
"checkpoints": self.checkpoint_count,
|
226
|
+
}
|
227
|
+
|
228
|
+
except asyncio.CancelledError:
|
229
|
+
await self._handle_cancellation()
|
230
|
+
raise
|
231
|
+
|
232
|
+
except Exception as e:
|
233
|
+
await self._handle_error(e)
|
234
|
+
raise
|
235
|
+
|
236
|
+
async def resume(self, checkpoint_id: Optional[str] = None) -> Dict[str, Any]:
|
237
|
+
"""Resume execution from checkpoint."""
|
238
|
+
self.state = RequestState.RESUMING
|
239
|
+
await self.journal.record(
|
240
|
+
"request_resuming",
|
241
|
+
{
|
242
|
+
"request_id": self.id,
|
243
|
+
"checkpoint_id": checkpoint_id,
|
244
|
+
},
|
245
|
+
)
|
246
|
+
|
247
|
+
try:
|
248
|
+
# Restore from checkpoint
|
249
|
+
if checkpoint_id:
|
250
|
+
checkpoint = await self._restore_checkpoint(checkpoint_id)
|
251
|
+
else:
|
252
|
+
# Use latest checkpoint
|
253
|
+
checkpoint = await self._restore_latest_checkpoint()
|
254
|
+
|
255
|
+
if not checkpoint:
|
256
|
+
raise ValueError("No checkpoint found to resume from")
|
257
|
+
|
258
|
+
# Restore state
|
259
|
+
self.state = checkpoint.state
|
260
|
+
if checkpoint.workflow_state:
|
261
|
+
await self._restore_workflow_state(checkpoint.workflow_state)
|
262
|
+
|
263
|
+
# Continue execution
|
264
|
+
return await self.execute()
|
265
|
+
|
266
|
+
except Exception as e:
|
267
|
+
await self._handle_error(e)
|
268
|
+
raise
|
269
|
+
|
270
|
+
async def cancel(self):
|
271
|
+
"""Cancel the request execution."""
|
272
|
+
self._cancel_event.set()
|
273
|
+
self.state = RequestState.CANCELLED
|
274
|
+
await self.journal.record(
|
275
|
+
"request_cancelled",
|
276
|
+
{
|
277
|
+
"request_id": self.id,
|
278
|
+
},
|
279
|
+
)
|
280
|
+
|
281
|
+
if self.workflow and self.runtime:
|
282
|
+
# TODO: Implement workflow cancellation
|
283
|
+
pass
|
284
|
+
|
285
|
+
async def checkpoint(self, name: str, data: Dict[str, Any] = None) -> str:
|
286
|
+
"""Create a checkpoint."""
|
287
|
+
if self._cancel_event.is_set():
|
288
|
+
raise asyncio.CancelledError("Request was cancelled")
|
289
|
+
|
290
|
+
checkpoint = Checkpoint(
|
291
|
+
checkpoint_id=f"ckpt_{uuid.uuid4().hex[:12]}",
|
292
|
+
request_id=self.id,
|
293
|
+
sequence=self.checkpoint_count,
|
294
|
+
name=name,
|
295
|
+
state=self.state,
|
296
|
+
data=data or {},
|
297
|
+
workflow_state=(
|
298
|
+
await self._capture_workflow_state() if self.workflow else None
|
299
|
+
),
|
300
|
+
created_at=datetime.now(dt.UTC),
|
301
|
+
size_bytes=len(json.dumps(data or {})),
|
302
|
+
)
|
303
|
+
|
304
|
+
self.checkpoints.append(checkpoint)
|
305
|
+
self.checkpoint_count += 1
|
306
|
+
|
307
|
+
# Save to checkpoint manager if available
|
308
|
+
if self.checkpoint_manager:
|
309
|
+
await self.checkpoint_manager.save_checkpoint(checkpoint)
|
310
|
+
|
311
|
+
await self.journal.record(
|
312
|
+
"checkpoint_created",
|
313
|
+
{
|
314
|
+
"checkpoint_id": checkpoint.checkpoint_id,
|
315
|
+
"name": name,
|
316
|
+
"sequence": checkpoint.sequence,
|
317
|
+
},
|
318
|
+
)
|
319
|
+
|
320
|
+
logger.info(f"Created checkpoint {name} for request {self.id}")
|
321
|
+
return checkpoint.checkpoint_id
|
322
|
+
|
323
|
+
async def _validate_request(self):
|
324
|
+
"""Validate the request."""
|
325
|
+
self.state = RequestState.VALIDATED
|
326
|
+
await self.checkpoint(
|
327
|
+
"request_validated",
|
328
|
+
{
|
329
|
+
"metadata": {
|
330
|
+
"method": self.metadata.method,
|
331
|
+
"path": self.metadata.path,
|
332
|
+
"idempotency_key": self.metadata.idempotency_key,
|
333
|
+
}
|
334
|
+
},
|
335
|
+
)
|
336
|
+
|
337
|
+
async def _create_workflow(self):
|
338
|
+
"""Create workflow from request."""
|
339
|
+
# This is a simplified example - in practice, this would
|
340
|
+
# parse the request and create appropriate workflow
|
341
|
+
if not self.metadata.body:
|
342
|
+
raise ValueError("Request body required for workflow creation")
|
343
|
+
|
344
|
+
workflow_config = self.metadata.body.get("workflow", {})
|
345
|
+
|
346
|
+
# Create workflow based on configuration
|
347
|
+
self.workflow = Workflow(
|
348
|
+
workflow_id=f"wf_{self.id}",
|
349
|
+
name=workflow_config.get("name", "DurableWorkflow"),
|
350
|
+
)
|
351
|
+
|
352
|
+
# TODO: Add nodes based on workflow config
|
353
|
+
# This would parse the request and build the workflow
|
354
|
+
|
355
|
+
self.workflow_id = self.workflow.workflow_id
|
356
|
+
self.state = RequestState.WORKFLOW_CREATED
|
357
|
+
|
358
|
+
await self.checkpoint(
|
359
|
+
"workflow_created",
|
360
|
+
{
|
361
|
+
"workflow_id": self.workflow_id,
|
362
|
+
"node_count": len(self.workflow.nodes),
|
363
|
+
},
|
364
|
+
)
|
365
|
+
|
366
|
+
async def _execute_workflow(self) -> Dict[str, Any]:
|
367
|
+
"""Execute workflow with checkpointing."""
|
368
|
+
self.state = RequestState.EXECUTING
|
369
|
+
self.runtime = LocalRuntime()
|
370
|
+
|
371
|
+
# Execute workflow
|
372
|
+
# TODO: Implement checkpoint-aware execution
|
373
|
+
# For now, standard execution
|
374
|
+
result, run_id = await self.runtime.execute(self.workflow)
|
375
|
+
|
376
|
+
# Checkpoint final result
|
377
|
+
await self.checkpoint(
|
378
|
+
"workflow_completed",
|
379
|
+
{
|
380
|
+
"run_id": run_id,
|
381
|
+
"result": result,
|
382
|
+
},
|
383
|
+
)
|
384
|
+
|
385
|
+
return result
|
386
|
+
|
387
|
+
async def _capture_workflow_state(self) -> Dict[str, Any]:
|
388
|
+
"""Capture current workflow state."""
|
389
|
+
if not self.workflow:
|
390
|
+
return {}
|
391
|
+
|
392
|
+
# TODO: Implement workflow state capture
|
393
|
+
# This would include:
|
394
|
+
# - Completed nodes
|
395
|
+
# - Node outputs
|
396
|
+
# - Workflow variables
|
397
|
+
# - Execution context
|
398
|
+
|
399
|
+
return {
|
400
|
+
"workflow_id": self.workflow_id,
|
401
|
+
"completed_nodes": [],
|
402
|
+
"node_outputs": {},
|
403
|
+
}
|
404
|
+
|
405
|
+
async def _restore_checkpoint(self, checkpoint_id: str) -> Optional[Checkpoint]:
|
406
|
+
"""Restore from specific checkpoint."""
|
407
|
+
if self.checkpoint_manager:
|
408
|
+
return await self.checkpoint_manager.load_checkpoint(checkpoint_id)
|
409
|
+
|
410
|
+
# Search in memory
|
411
|
+
for ckpt in self.checkpoints:
|
412
|
+
if ckpt.checkpoint_id == checkpoint_id:
|
413
|
+
return ckpt
|
414
|
+
|
415
|
+
return None
|
416
|
+
|
417
|
+
async def _restore_latest_checkpoint(self) -> Optional[Checkpoint]:
|
418
|
+
"""Restore from latest checkpoint."""
|
419
|
+
if self.checkpoint_manager:
|
420
|
+
return await self.checkpoint_manager.load_latest_checkpoint(self.id)
|
421
|
+
|
422
|
+
# Use in-memory checkpoint
|
423
|
+
return self.checkpoints[-1] if self.checkpoints else None
|
424
|
+
|
425
|
+
async def _restore_workflow_state(self, workflow_state: Dict[str, Any]):
|
426
|
+
"""Restore workflow state from checkpoint."""
|
427
|
+
# TODO: Implement workflow state restoration
|
428
|
+
# This would restore:
|
429
|
+
# - Node execution state
|
430
|
+
# - Intermediate results
|
431
|
+
# - Workflow variables
|
432
|
+
pass
|
433
|
+
|
434
|
+
async def _handle_cancellation(self):
|
435
|
+
"""Handle request cancellation."""
|
436
|
+
self.state = RequestState.CANCELLED
|
437
|
+
self.end_time = time.time()
|
438
|
+
|
439
|
+
await self.checkpoint(
|
440
|
+
"request_cancelled",
|
441
|
+
{
|
442
|
+
"duration_ms": (
|
443
|
+
(self.end_time - self.start_time) * 1000 if self.start_time else 0
|
444
|
+
),
|
445
|
+
},
|
446
|
+
)
|
447
|
+
|
448
|
+
await self.journal.record(
|
449
|
+
"request_cancelled",
|
450
|
+
{
|
451
|
+
"request_id": self.id,
|
452
|
+
"checkpoints_created": self.checkpoint_count,
|
453
|
+
},
|
454
|
+
)
|
455
|
+
|
456
|
+
async def _handle_error(self, error: Exception):
|
457
|
+
"""Handle execution error."""
|
458
|
+
self.state = RequestState.FAILED
|
459
|
+
self.end_time = time.time()
|
460
|
+
self.error = error
|
461
|
+
|
462
|
+
await self.checkpoint(
|
463
|
+
"request_failed",
|
464
|
+
{
|
465
|
+
"error": str(error),
|
466
|
+
"error_type": type(error).__name__,
|
467
|
+
},
|
468
|
+
)
|
469
|
+
|
470
|
+
await self.journal.record(
|
471
|
+
"request_failed",
|
472
|
+
{
|
473
|
+
"request_id": self.id,
|
474
|
+
"error": str(error),
|
475
|
+
"error_type": type(error).__name__,
|
476
|
+
"duration_ms": (
|
477
|
+
(self.end_time - self.start_time) * 1000 if self.start_time else 0
|
478
|
+
),
|
479
|
+
},
|
480
|
+
)
|
481
|
+
|
482
|
+
logger.error(f"Request {self.id} failed: {error}")
|
483
|
+
|
484
|
+
def get_status(self) -> Dict[str, Any]:
|
485
|
+
"""Get current request status."""
|
486
|
+
return {
|
487
|
+
"request_id": self.id,
|
488
|
+
"state": self.state.value,
|
489
|
+
"checkpoints": self.checkpoint_count,
|
490
|
+
"events": len(self.journal.events),
|
491
|
+
"duration_ms": (
|
492
|
+
(self.end_time - self.start_time) * 1000
|
493
|
+
if self.start_time and self.end_time
|
494
|
+
else None
|
495
|
+
),
|
496
|
+
"result": self.result,
|
497
|
+
"error": str(self.error) if self.error else None,
|
498
|
+
}
|