dory-processor-sdk 0.0.1__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.
Files changed (86) hide show
  1. dory/__init__.py +101 -0
  2. dory/auth/__init__.py +10 -0
  3. dory/auth/oauth2.py +153 -0
  4. dory/auto_instrument.py +142 -0
  5. dory/cli/__init__.py +5 -0
  6. dory/cli/main.py +137 -0
  7. dory/cli/templates.py +123 -0
  8. dory/config/__init__.py +23 -0
  9. dory/config/defaults.py +24 -0
  10. dory/config/loader.py +430 -0
  11. dory/config/presets.py +73 -0
  12. dory/config/schema.py +84 -0
  13. dory/core/__init__.py +27 -0
  14. dory/core/app.py +434 -0
  15. dory/core/context.py +209 -0
  16. dory/core/lifecycle.py +214 -0
  17. dory/core/meta.py +121 -0
  18. dory/core/modes.py +479 -0
  19. dory/core/processor.py +564 -0
  20. dory/core/signals.py +122 -0
  21. dory/decorators.py +142 -0
  22. dory/edge/__init__.py +88 -0
  23. dory/edge/adaptive.py +644 -0
  24. dory/edge/detector.py +546 -0
  25. dory/edge/fencing.py +488 -0
  26. dory/edge/heartbeat.py +598 -0
  27. dory/edge/role.py +419 -0
  28. dory/errors/__init__.py +139 -0
  29. dory/errors/classification.py +362 -0
  30. dory/errors/codes.py +498 -0
  31. dory/geo/__init__.py +40 -0
  32. dory/geo/geolocalizer.py +1034 -0
  33. dory/health/__init__.py +12 -0
  34. dory/health/probes.py +210 -0
  35. dory/health/server.py +635 -0
  36. dory/k8s/__init__.py +80 -0
  37. dory/k8s/annotation_watcher.py +184 -0
  38. dory/k8s/client.py +251 -0
  39. dory/k8s/labels.py +505 -0
  40. dory/k8s/pod_metadata.py +182 -0
  41. dory/logging/__init__.py +9 -0
  42. dory/logging/logger.py +148 -0
  43. dory/metrics/__init__.py +7 -0
  44. dory/metrics/collector.py +301 -0
  45. dory/middleware/__init__.py +46 -0
  46. dory/middleware/connection_tracker.py +608 -0
  47. dory/middleware/request_id.py +325 -0
  48. dory/middleware/request_tracker.py +511 -0
  49. dory/migration/__init__.py +33 -0
  50. dory/migration/configmap.py +232 -0
  51. dory/migration/s3_store.py +594 -0
  52. dory/migration/serialization.py +135 -0
  53. dory/migration/state_manager.py +286 -0
  54. dory/migration/transfer.py +382 -0
  55. dory/monitoring/__init__.py +29 -0
  56. dory/monitoring/opentelemetry.py +489 -0
  57. dory/output/__init__.py +31 -0
  58. dory/output/envelope.py +137 -0
  59. dory/output/formatter.py +113 -0
  60. dory/output/rabbitmq.py +632 -0
  61. dory/output/routing.py +318 -0
  62. dory/output/validator.py +199 -0
  63. dory/py.typed +2 -0
  64. dory/recovery/__init__.py +60 -0
  65. dory/recovery/golden_image.py +487 -0
  66. dory/recovery/golden_snapshot.py +713 -0
  67. dory/recovery/golden_validator.py +518 -0
  68. dory/recovery/partial_recovery.py +482 -0
  69. dory/recovery/recovery_decision.py +242 -0
  70. dory/recovery/restart_detector.py +142 -0
  71. dory/recovery/state_validator.py +183 -0
  72. dory/resilience/__init__.py +45 -0
  73. dory/resilience/circuit_breaker.py +457 -0
  74. dory/resilience/retry.py +389 -0
  75. dory/simple.py +342 -0
  76. dory/types.py +68 -0
  77. dory/utils/__init__.py +31 -0
  78. dory/utils/errors.py +59 -0
  79. dory/utils/retry.py +115 -0
  80. dory/utils/timeout.py +80 -0
  81. dory_processor_sdk-0.0.1.dist-info/METADATA +424 -0
  82. dory_processor_sdk-0.0.1.dist-info/RECORD +86 -0
  83. dory_processor_sdk-0.0.1.dist-info/WHEEL +5 -0
  84. dory_processor_sdk-0.0.1.dist-info/entry_points.txt +2 -0
  85. dory_processor_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
  86. dory_processor_sdk-0.0.1.dist-info/top_level.txt +1 -0
dory/edge/adaptive.py ADDED
@@ -0,0 +1,644 @@
1
+ """Adaptive behavior for edge vs cloud environments.
2
+
3
+ Provides location-aware processing that automatically adjusts behavior
4
+ based on whether the workload is running on edge or cloud nodes.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import Any, Callable, Coroutine
12
+
13
+ from dory.edge.detector import (
14
+ WorkloadContext,
15
+ WorkloadDetector,
16
+ NodeType,
17
+ get_workload_context,
18
+ )
19
+ from dory.edge.heartbeat import (
20
+ HeartbeatConfig,
21
+ HeartbeatManager,
22
+ ConnectivityStatus,
23
+ )
24
+ from dory.edge.fencing import FencingConfig, FencingManager
25
+ from dory.edge.role import RoleManager, ProcessorRole
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class OperationMode(Enum):
31
+ """Operating mode based on location and connectivity."""
32
+
33
+ EDGE_CONNECTED = "edge_connected" # Edge node, orchestrator reachable
34
+ EDGE_OFFLINE = "edge_offline" # Edge node, orchestrator unreachable
35
+ CLOUD_NORMAL = "cloud_normal" # Cloud node, normal operation
36
+ CLOUD_FAILOVER = "cloud_failover" # Cloud node, handling edge failover
37
+ DEGRADED = "degraded" # Unknown state, conservative mode
38
+
39
+
40
+ @dataclass
41
+ class EdgeConfig:
42
+ """Configuration for edge-specific behavior."""
43
+
44
+ # Heartbeat settings for edge (more aggressive)
45
+ edge_heartbeat_interval_sec: float = 5.0
46
+ edge_heartbeat_timeout_sec: float = 10.0
47
+ edge_max_missed_heartbeats: int = 3
48
+
49
+ # Heartbeat settings for cloud (standard)
50
+ cloud_heartbeat_interval_sec: float = 15.0
51
+ cloud_heartbeat_timeout_sec: float = 30.0
52
+ cloud_max_missed_heartbeats: int = 2
53
+
54
+ # Offline buffer settings (edge only)
55
+ offline_buffer_enabled: bool = True
56
+ offline_buffer_max_size_mb: int = 100
57
+ offline_buffer_flush_interval_sec: float = 30.0
58
+
59
+ # State checkpoint settings
60
+ edge_checkpoint_interval_sec: float = 10.0 # More frequent on edge
61
+ cloud_checkpoint_interval_sec: float = 60.0 # Less frequent on cloud
62
+
63
+ # Retry settings
64
+ edge_max_retries: int = 5
65
+ edge_retry_backoff_sec: float = 1.0
66
+ cloud_max_retries: int = 3
67
+ cloud_retry_backoff_sec: float = 2.0
68
+
69
+ # Resource constraints
70
+ edge_max_batch_size: int = 100 # Smaller batches on edge
71
+ cloud_max_batch_size: int = 1000 # Larger batches on cloud
72
+ edge_max_concurrent: int = 2 # Limited concurrency on edge
73
+ cloud_max_concurrent: int = 10 # Higher concurrency on cloud
74
+
75
+
76
+ @dataclass
77
+ class AdaptiveConfig:
78
+ """Full configuration for adaptive processor."""
79
+
80
+ app_name: str
81
+ processor_id: str
82
+ orchestrator_url: str | None = None
83
+ edge_config: EdgeConfig = field(default_factory=EdgeConfig)
84
+ fencing_config: FencingConfig | None = None
85
+ custom_detector: WorkloadDetector | None = None
86
+
87
+
88
+ class AdaptiveProcessor:
89
+ """Processor that adapts behavior based on edge/cloud location.
90
+
91
+ Automatically adjusts:
92
+ - Heartbeat intervals and retry behavior
93
+ - State checkpoint frequency
94
+ - Batch sizes and concurrency
95
+ - Offline buffering (edge only)
96
+ - Failover handling
97
+
98
+ Usage:
99
+ processor = AdaptiveProcessor(AdaptiveConfig(
100
+ app_name="my-app",
101
+ processor_id="processor-1",
102
+ orchestrator_url="http://orchestrator:8080",
103
+ ))
104
+
105
+ await processor.start()
106
+
107
+ # Processor automatically adapts based on location
108
+ if processor.is_edge:
109
+ # Running on edge with edge-optimized settings
110
+ pass
111
+
112
+ if processor.is_offline:
113
+ # Edge node offline, using local buffer
114
+ pass
115
+ """
116
+
117
+ def __init__(self, config: AdaptiveConfig):
118
+ self.config = config
119
+ self._detector = config.custom_detector or WorkloadDetector()
120
+ self._context: WorkloadContext | None = None
121
+ self._mode = OperationMode.DEGRADED
122
+ self._heartbeat: HeartbeatManager | None = None
123
+ self._fencing: FencingManager | None = None
124
+ self._role_manager: RoleManager | None = None
125
+ self._offline_buffer: list[dict[str, Any]] = []
126
+ self._state: dict[str, Any] = {}
127
+ self._running = False
128
+ self._checkpoint_task: asyncio.Task | None = None
129
+ self._flush_task: asyncio.Task | None = None
130
+
131
+ # Callbacks
132
+ self._on_mode_change: Callable[[OperationMode, OperationMode], Coroutine] | None = None
133
+ self._on_connectivity_change: Callable[[ConnectivityStatus], Coroutine] | None = None
134
+ self._on_failover: Callable[[str], Coroutine] | None = None # original_node
135
+ self._on_failback: Callable[[], Coroutine] | None = None
136
+
137
+ # =========================================================================
138
+ # Properties
139
+ # =========================================================================
140
+
141
+ @property
142
+ def context(self) -> WorkloadContext | None:
143
+ """Get current workload context."""
144
+ return self._context
145
+
146
+ @property
147
+ def mode(self) -> OperationMode:
148
+ """Get current operation mode."""
149
+ return self._mode
150
+
151
+ @property
152
+ def is_edge(self) -> bool:
153
+ """Check if running on edge node."""
154
+ return self._context.is_edge if self._context else False
155
+
156
+ @property
157
+ def is_cloud(self) -> bool:
158
+ """Check if running on cloud node."""
159
+ return not self.is_edge
160
+
161
+ @property
162
+ def is_migrated(self) -> bool:
163
+ """Check if this is a migrated (failover) workload."""
164
+ return self._context.is_migrated if self._context else False
165
+
166
+ @property
167
+ def is_offline(self) -> bool:
168
+ """Check if currently offline (edge only)."""
169
+ return self._mode == OperationMode.EDGE_OFFLINE
170
+
171
+ @property
172
+ def is_connected(self) -> bool:
173
+ """Check if connected to orchestrator."""
174
+ if self._heartbeat:
175
+ return self._heartbeat.is_connected()
176
+ return False
177
+
178
+ @property
179
+ def connectivity_status(self) -> ConnectivityStatus:
180
+ """Get current connectivity status."""
181
+ if self._heartbeat:
182
+ return self._heartbeat.status
183
+ return ConnectivityStatus.UNKNOWN
184
+
185
+ @property
186
+ def role(self) -> ProcessorRole:
187
+ """Get current processor role."""
188
+ if self._role_manager:
189
+ return self._role_manager.role
190
+ return ProcessorRole.INITIALIZING
191
+
192
+ # =========================================================================
193
+ # Configuration Helpers
194
+ # =========================================================================
195
+
196
+ def get_heartbeat_config(self) -> HeartbeatConfig:
197
+ """Get heartbeat config based on location."""
198
+ ec = self.config.edge_config
199
+
200
+ if self.is_edge:
201
+ return HeartbeatConfig(
202
+ interval_sec=ec.edge_heartbeat_interval_sec,
203
+ timeout_sec=ec.edge_heartbeat_timeout_sec,
204
+ missed_threshold=ec.edge_max_missed_heartbeats,
205
+ )
206
+ else:
207
+ return HeartbeatConfig(
208
+ interval_sec=ec.cloud_heartbeat_interval_sec,
209
+ timeout_sec=ec.cloud_heartbeat_timeout_sec,
210
+ missed_threshold=ec.cloud_max_missed_heartbeats,
211
+ )
212
+
213
+ def get_checkpoint_interval(self) -> float:
214
+ """Get checkpoint interval based on location."""
215
+ ec = self.config.edge_config
216
+ return ec.edge_checkpoint_interval_sec if self.is_edge else ec.cloud_checkpoint_interval_sec
217
+
218
+ def get_max_batch_size(self) -> int:
219
+ """Get max batch size based on location."""
220
+ ec = self.config.edge_config
221
+ return ec.edge_max_batch_size if self.is_edge else ec.cloud_max_batch_size
222
+
223
+ def get_max_concurrent(self) -> int:
224
+ """Get max concurrency based on location."""
225
+ ec = self.config.edge_config
226
+ return ec.edge_max_concurrent if self.is_edge else ec.cloud_max_concurrent
227
+
228
+ def get_retry_config(self) -> tuple[int, float]:
229
+ """Get retry config (max_retries, backoff_sec) based on location."""
230
+ ec = self.config.edge_config
231
+ if self.is_edge:
232
+ return ec.edge_max_retries, ec.edge_retry_backoff_sec
233
+ return ec.cloud_max_retries, ec.cloud_retry_backoff_sec
234
+
235
+ # =========================================================================
236
+ # Lifecycle
237
+ # =========================================================================
238
+
239
+ async def start(self) -> OperationMode:
240
+ """Start the adaptive processor.
241
+
242
+ Returns:
243
+ Initial operation mode
244
+ """
245
+ logger.info(f"Starting adaptive processor: {self.config.processor_id}")
246
+
247
+ # Detect workload context
248
+ self._context = self._detector.detect()
249
+ logger.info(
250
+ f"Workload context: node_type={self._context.node_type.value}, "
251
+ f"is_edge={self._context.is_edge}, is_migrated={self._context.is_migrated}"
252
+ )
253
+
254
+ # Initialize fencing
255
+ fencing_config = self.config.fencing_config or FencingConfig()
256
+ self._fencing = FencingManager(config=fencing_config)
257
+
258
+ # Initialize role manager
259
+ node_id = self._context.node_name if self._context else "unknown"
260
+ self._role_manager = RoleManager(
261
+ processor_id=self.config.processor_id,
262
+ node_id=node_id,
263
+ fencing_config=fencing_config,
264
+ )
265
+
266
+ # Initialize heartbeat if orchestrator URL provided
267
+ if self.config.orchestrator_url:
268
+ heartbeat_config = self.get_heartbeat_config()
269
+ heartbeat_config.orchestrator_url = self.config.orchestrator_url
270
+ self._heartbeat = HeartbeatManager(config=heartbeat_config)
271
+
272
+ # Register connectivity callback
273
+ if self._on_connectivity_change:
274
+ # HeartbeatManager would need to support this
275
+ pass
276
+
277
+ # Handle migrated workload
278
+ if self._context.is_migrated:
279
+ logger.info(f"Migrated workload from: {self._context.original_node}")
280
+ if self._on_failover:
281
+ await self._on_failover(self._context.original_node or "unknown")
282
+
283
+ # Start role manager (acquires fencing token)
284
+ await self._role_manager.start()
285
+
286
+ # Start heartbeat
287
+ if self._heartbeat:
288
+ await self._heartbeat.start()
289
+
290
+ # Determine initial mode
291
+ self._mode = self._determine_mode()
292
+ logger.info(f"Initial operation mode: {self._mode.value}")
293
+
294
+ # Start background tasks
295
+ self._running = True
296
+ self._checkpoint_task = asyncio.create_task(self._checkpoint_loop())
297
+
298
+ if self.is_edge and self.config.edge_config.offline_buffer_enabled:
299
+ self._flush_task = asyncio.create_task(self._flush_loop())
300
+
301
+ return self._mode
302
+
303
+ async def stop(self, reason: str = "shutdown") -> None:
304
+ """Stop the adaptive processor.
305
+
306
+ Args:
307
+ reason: Reason for stopping
308
+ """
309
+ logger.info(f"Stopping adaptive processor: {reason}")
310
+ self._running = False
311
+
312
+ # Cancel background tasks
313
+ if self._checkpoint_task:
314
+ self._checkpoint_task.cancel()
315
+ try:
316
+ await self._checkpoint_task
317
+ except asyncio.CancelledError:
318
+ pass
319
+
320
+ if self._flush_task:
321
+ self._flush_task.cancel()
322
+ try:
323
+ await self._flush_task
324
+ except asyncio.CancelledError:
325
+ pass
326
+
327
+ # Flush any remaining offline buffer
328
+ if self._offline_buffer:
329
+ await self._flush_offline_buffer()
330
+
331
+ # Final state checkpoint
332
+ await self._save_state()
333
+
334
+ # Stop heartbeat
335
+ if self._heartbeat:
336
+ await self._heartbeat.stop()
337
+
338
+ # Release fencing
339
+ if self._role_manager:
340
+ await self._role_manager.stop(reason)
341
+
342
+ logger.info("Adaptive processor stopped")
343
+
344
+ # =========================================================================
345
+ # Mode Management
346
+ # =========================================================================
347
+
348
+ def _determine_mode(self) -> OperationMode:
349
+ """Determine current operation mode."""
350
+ if not self._context:
351
+ return OperationMode.DEGRADED
352
+
353
+ if self._context.is_edge:
354
+ # Edge node
355
+ if self.is_connected:
356
+ return OperationMode.EDGE_CONNECTED
357
+ else:
358
+ return OperationMode.EDGE_OFFLINE
359
+ else:
360
+ # Cloud node
361
+ if self._context.is_migrated:
362
+ return OperationMode.CLOUD_FAILOVER
363
+ else:
364
+ return OperationMode.CLOUD_NORMAL
365
+
366
+ async def _update_mode(self) -> None:
367
+ """Update operation mode and trigger callbacks if changed."""
368
+ old_mode = self._mode
369
+ new_mode = self._determine_mode()
370
+
371
+ if old_mode != new_mode:
372
+ logger.info(f"Mode change: {old_mode.value} -> {new_mode.value}")
373
+ self._mode = new_mode
374
+
375
+ if self._on_mode_change:
376
+ await self._on_mode_change(old_mode, new_mode)
377
+
378
+ # Handle specific transitions
379
+ if old_mode == OperationMode.EDGE_OFFLINE and new_mode == OperationMode.EDGE_CONNECTED:
380
+ # Came back online - flush buffer
381
+ await self._flush_offline_buffer()
382
+
383
+ # =========================================================================
384
+ # State Management
385
+ # =========================================================================
386
+
387
+ def get_state(self) -> dict[str, Any]:
388
+ """Get current processor state."""
389
+ return self._state.copy()
390
+
391
+ def set_state(self, state: dict[str, Any]) -> None:
392
+ """Set processor state."""
393
+ self._state = state.copy()
394
+
395
+ async def restore_state(self, state: dict[str, Any]) -> None:
396
+ """Restore state (called during failover recovery)."""
397
+ self._state = state.copy()
398
+ logger.info(f"State restored: {len(state)} keys")
399
+
400
+ async def _save_state(self) -> None:
401
+ """Save state to appropriate storage."""
402
+ if self.is_offline:
403
+ # Buffer locally when offline
404
+ self._buffer_state(self._state)
405
+ else:
406
+ # Save to remote storage
407
+ await self._save_state_remote(self._state)
408
+
409
+ async def _save_state_remote(self, state: dict[str, Any]) -> None:
410
+ """Save state to remote storage (orchestrator/S3)."""
411
+ # This would integrate with StateManager
412
+ logger.debug(f"Saving state remotely: {len(state)} keys")
413
+
414
+ def _buffer_state(self, state: dict[str, Any]) -> None:
415
+ """Buffer state locally (edge offline mode)."""
416
+ max_size = self.config.edge_config.offline_buffer_max_size_mb * 1024 * 1024
417
+ # Simplified size check
418
+ if len(self._offline_buffer) < 10000: # Rough limit
419
+ self._offline_buffer.append(state.copy())
420
+ logger.debug(f"State buffered locally: {len(self._offline_buffer)} items")
421
+
422
+ async def _flush_offline_buffer(self) -> None:
423
+ """Flush offline buffer to remote storage."""
424
+ if not self._offline_buffer:
425
+ return
426
+
427
+ logger.info(f"Flushing offline buffer: {len(self._offline_buffer)} items")
428
+
429
+ # In real implementation, this would batch-upload to S3/orchestrator
430
+ flushed = 0
431
+ while self._offline_buffer and self.is_connected:
432
+ state = self._offline_buffer.pop(0)
433
+ try:
434
+ await self._save_state_remote(state)
435
+ flushed += 1
436
+ except Exception as e:
437
+ # Put back and retry later
438
+ self._offline_buffer.insert(0, state)
439
+ logger.warning(f"Buffer flush failed: {e}")
440
+ break
441
+
442
+ logger.info(f"Flushed {flushed} items from offline buffer")
443
+
444
+ # =========================================================================
445
+ # Background Tasks
446
+ # =========================================================================
447
+
448
+ async def _checkpoint_loop(self) -> None:
449
+ """Periodic state checkpoint."""
450
+ interval = self.get_checkpoint_interval()
451
+
452
+ while self._running:
453
+ try:
454
+ await asyncio.sleep(interval)
455
+ await self._save_state()
456
+ await self._update_mode()
457
+ except asyncio.CancelledError:
458
+ break
459
+ except Exception as e:
460
+ logger.error(f"Checkpoint error: {e}")
461
+
462
+ async def _flush_loop(self) -> None:
463
+ """Periodic offline buffer flush (edge only)."""
464
+ interval = self.config.edge_config.offline_buffer_flush_interval_sec
465
+
466
+ while self._running:
467
+ try:
468
+ await asyncio.sleep(interval)
469
+ if self.is_connected and self._offline_buffer:
470
+ await self._flush_offline_buffer()
471
+ except asyncio.CancelledError:
472
+ break
473
+ except Exception as e:
474
+ logger.error(f"Flush error: {e}")
475
+
476
+ # =========================================================================
477
+ # Callbacks
478
+ # =========================================================================
479
+
480
+ def on_mode_change(
481
+ self, callback: Callable[[OperationMode, OperationMode], Coroutine]
482
+ ) -> None:
483
+ """Register callback for mode changes."""
484
+ self._on_mode_change = callback
485
+
486
+ def on_connectivity_change(
487
+ self, callback: Callable[[ConnectivityStatus], Coroutine]
488
+ ) -> None:
489
+ """Register callback for connectivity changes."""
490
+ self._on_connectivity_change = callback
491
+
492
+ def on_failover(self, callback: Callable[[str], Coroutine]) -> None:
493
+ """Register callback for failover (receives original_node)."""
494
+ self._on_failover = callback
495
+
496
+ def on_failback(self, callback: Callable[[], Coroutine]) -> None:
497
+ """Register callback for failback to edge."""
498
+ self._on_failback = callback
499
+
500
+ # =========================================================================
501
+ # Processing Helpers
502
+ # =========================================================================
503
+
504
+ async def process_with_fencing(
505
+ self,
506
+ operation: Callable[[], Coroutine[Any, Any, Any]],
507
+ ) -> Any:
508
+ """Execute operation with fencing validation.
509
+
510
+ Validates fencing token before and after operation to ensure
511
+ we're still the primary processor.
512
+
513
+ Args:
514
+ operation: Async operation to execute
515
+
516
+ Returns:
517
+ Operation result
518
+
519
+ Raises:
520
+ FenceViolation: If fencing is violated
521
+ """
522
+ if not self._role_manager:
523
+ raise RuntimeError("Processor not started")
524
+
525
+ # Validate before
526
+ if not await self._role_manager.validate_fencing_or_fence():
527
+ from dory.edge.fencing import FenceViolation
528
+ raise FenceViolation("Fencing validation failed before operation")
529
+
530
+ # Execute operation
531
+ result = await operation()
532
+
533
+ # Validate after
534
+ if not await self._role_manager.validate_fencing_or_fence():
535
+ from dory.edge.fencing import FenceViolation
536
+ raise FenceViolation("Fencing validation failed after operation")
537
+
538
+ return result
539
+
540
+ async def process_batch(
541
+ self,
542
+ items: list[Any],
543
+ processor: Callable[[Any], Coroutine[Any, Any, Any]],
544
+ ) -> list[Any]:
545
+ """Process items in batches appropriate for current location.
546
+
547
+ Automatically adjusts batch size and concurrency based on
548
+ whether running on edge or cloud.
549
+
550
+ Args:
551
+ items: Items to process
552
+ processor: Async function to process each item
553
+
554
+ Returns:
555
+ List of results
556
+ """
557
+ batch_size = self.get_max_batch_size()
558
+ max_concurrent = self.get_max_concurrent()
559
+
560
+ results = []
561
+ semaphore = asyncio.Semaphore(max_concurrent)
562
+
563
+ async def process_with_semaphore(item: Any) -> Any:
564
+ async with semaphore:
565
+ return await processor(item)
566
+
567
+ # Process in batches
568
+ for i in range(0, len(items), batch_size):
569
+ batch = items[i:i + batch_size]
570
+ batch_results = await asyncio.gather(
571
+ *[process_with_semaphore(item) for item in batch],
572
+ return_exceptions=True,
573
+ )
574
+ results.extend(batch_results)
575
+
576
+ # Checkpoint after each batch
577
+ if self.is_edge:
578
+ await self._save_state()
579
+
580
+ return results
581
+
582
+
583
+ # =============================================================================
584
+ # Convenience Functions
585
+ # =============================================================================
586
+
587
+ def create_adaptive_processor(
588
+ app_name: str,
589
+ processor_id: str,
590
+ orchestrator_url: str | None = None,
591
+ **kwargs: Any,
592
+ ) -> AdaptiveProcessor:
593
+ """Create an adaptive processor with sensible defaults.
594
+
595
+ Args:
596
+ app_name: Application name
597
+ processor_id: Unique processor identifier
598
+ orchestrator_url: URL of the orchestrator service
599
+ **kwargs: Additional EdgeConfig parameters
600
+
601
+ Returns:
602
+ Configured AdaptiveProcessor
603
+ """
604
+ edge_config = EdgeConfig(**kwargs) if kwargs else EdgeConfig()
605
+
606
+ config = AdaptiveConfig(
607
+ app_name=app_name,
608
+ processor_id=processor_id,
609
+ orchestrator_url=orchestrator_url,
610
+ edge_config=edge_config,
611
+ )
612
+
613
+ return AdaptiveProcessor(config)
614
+
615
+
616
+ def get_location_aware_settings() -> dict[str, Any]:
617
+ """Get settings appropriate for current location.
618
+
619
+ Returns:
620
+ Dictionary of settings adjusted for edge or cloud
621
+ """
622
+ context = get_workload_context()
623
+ config = EdgeConfig()
624
+
625
+ if context.is_edge:
626
+ return {
627
+ "heartbeat_interval_sec": config.edge_heartbeat_interval_sec,
628
+ "checkpoint_interval_sec": config.edge_checkpoint_interval_sec,
629
+ "max_batch_size": config.edge_max_batch_size,
630
+ "max_concurrent": config.edge_max_concurrent,
631
+ "max_retries": config.edge_max_retries,
632
+ "retry_backoff_sec": config.edge_retry_backoff_sec,
633
+ "offline_buffer_enabled": config.offline_buffer_enabled,
634
+ }
635
+ else:
636
+ return {
637
+ "heartbeat_interval_sec": config.cloud_heartbeat_interval_sec,
638
+ "checkpoint_interval_sec": config.cloud_checkpoint_interval_sec,
639
+ "max_batch_size": config.cloud_max_batch_size,
640
+ "max_concurrent": config.cloud_max_concurrent,
641
+ "max_retries": config.cloud_max_retries,
642
+ "retry_backoff_sec": config.cloud_retry_backoff_sec,
643
+ "offline_buffer_enabled": False,
644
+ }