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/k8s/__init__.py ADDED
@@ -0,0 +1,80 @@
1
+ """Kubernetes integration utilities."""
2
+
3
+ from dory.k8s.client import K8sClient
4
+ from dory.k8s.pod_metadata import PodMetadata
5
+ from dory.k8s.annotation_watcher import AnnotationWatcher
6
+ from dory.k8s.labels import (
7
+ # Label keys
8
+ LABEL_MANAGED_BY,
9
+ LABEL_APP_NAME,
10
+ LABEL_PROCESSOR_ID,
11
+ LABEL_WORKLOAD_TYPE,
12
+ LABEL_WORKLOAD_LOCATION,
13
+ LABEL_NODE_TYPE,
14
+ LABEL_MIGRATED_FROM_EDGE,
15
+ LABEL_ORIGINAL_NODE,
16
+ # Label values
17
+ VALUE_ORCHESTRATOR_NAME,
18
+ VALUE_EDGE,
19
+ VALUE_MANAGED,
20
+ # Enums
21
+ WorkloadLocation,
22
+ NodeType,
23
+ # Builder
24
+ DoryLabels,
25
+ # Utilities
26
+ get_label,
27
+ get_app_name,
28
+ get_processor_id,
29
+ get_workload_location,
30
+ is_managed_by_dory,
31
+ is_edge_workload,
32
+ is_migrated_from_edge,
33
+ get_original_node,
34
+ detect_workload_context,
35
+ edge_node_selector,
36
+ managed_node_selector,
37
+ edge_toleration,
38
+ get_contract_documentation,
39
+ CONTRACT_VERSION,
40
+ )
41
+
42
+ __all__ = [
43
+ # Core
44
+ "K8sClient",
45
+ "PodMetadata",
46
+ "AnnotationWatcher",
47
+ # Label keys
48
+ "LABEL_MANAGED_BY",
49
+ "LABEL_APP_NAME",
50
+ "LABEL_PROCESSOR_ID",
51
+ "LABEL_WORKLOAD_TYPE",
52
+ "LABEL_WORKLOAD_LOCATION",
53
+ "LABEL_NODE_TYPE",
54
+ "LABEL_MIGRATED_FROM_EDGE",
55
+ "LABEL_ORIGINAL_NODE",
56
+ # Label values
57
+ "VALUE_ORCHESTRATOR_NAME",
58
+ "VALUE_EDGE",
59
+ "VALUE_MANAGED",
60
+ # Enums
61
+ "WorkloadLocation",
62
+ "NodeType",
63
+ # Builder
64
+ "DoryLabels",
65
+ # Utilities
66
+ "get_label",
67
+ "get_app_name",
68
+ "get_processor_id",
69
+ "get_workload_location",
70
+ "is_managed_by_dory",
71
+ "is_edge_workload",
72
+ "is_migrated_from_edge",
73
+ "get_original_node",
74
+ "detect_workload_context",
75
+ "edge_node_selector",
76
+ "managed_node_selector",
77
+ "edge_toleration",
78
+ "get_contract_documentation",
79
+ "CONTRACT_VERSION",
80
+ ]
@@ -0,0 +1,184 @@
1
+ """
2
+ Annotation watcher for migration signals.
3
+
4
+ Watches pod annotations for migration-related signals
5
+ from the orchestrator.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from typing import Callable, Any
11
+
12
+ from dory.k8s.client import K8sClient
13
+ from dory.utils.errors import DoryK8sError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class AnnotationWatcher:
19
+ """
20
+ Watches pod annotations for orchestrator signals.
21
+
22
+ Monitors annotations:
23
+ - dory.io/migration: "true" when migration imminent
24
+ - dory.io/shutdown: "true" when shutdown requested
25
+ - dory.io/snapshot: "true" when snapshot requested
26
+ """
27
+
28
+ MIGRATION_ANNOTATION = "dory.io/migration"
29
+ SHUTDOWN_ANNOTATION = "dory.io/shutdown"
30
+ SNAPSHOT_ANNOTATION = "dory.io/snapshot"
31
+ DEADLINE_ANNOTATION = "dory.io/migration-deadline"
32
+
33
+ def __init__(
34
+ self,
35
+ k8s_client: K8sClient,
36
+ pod_name: str,
37
+ poll_interval: float = 5.0,
38
+ ):
39
+ """
40
+ Initialize annotation watcher.
41
+
42
+ Args:
43
+ k8s_client: Kubernetes client
44
+ pod_name: Name of pod to watch
45
+ poll_interval: Seconds between polls
46
+ """
47
+ self._k8s_client = k8s_client
48
+ self._pod_name = pod_name
49
+ self._poll_interval = poll_interval
50
+
51
+ self._running = False
52
+ self._watch_task: asyncio.Task | None = None
53
+
54
+ # Callbacks
55
+ self._on_migration: Callable[[], Any] | None = None
56
+ self._on_shutdown: Callable[[], Any] | None = None
57
+ self._on_snapshot: Callable[[], Any] | None = None
58
+
59
+ # State tracking
60
+ self._last_annotations: dict[str, str] = {}
61
+
62
+ def on_migration(self, callback: Callable[[], Any]) -> None:
63
+ """Set callback for migration signal."""
64
+ self._on_migration = callback
65
+
66
+ def on_shutdown(self, callback: Callable[[], Any]) -> None:
67
+ """Set callback for shutdown signal."""
68
+ self._on_shutdown = callback
69
+
70
+ def on_snapshot(self, callback: Callable[[], Any]) -> None:
71
+ """Set callback for snapshot signal."""
72
+ self._on_snapshot = callback
73
+
74
+ async def start(self) -> None:
75
+ """Start watching annotations."""
76
+ if self._running:
77
+ return
78
+
79
+ self._running = True
80
+ self._watch_task = asyncio.create_task(self._watch_loop())
81
+ logger.info(f"Started annotation watcher for pod {self._pod_name}")
82
+
83
+ async def stop(self) -> None:
84
+ """Stop watching annotations."""
85
+ self._running = False
86
+
87
+ if self._watch_task:
88
+ self._watch_task.cancel()
89
+ try:
90
+ await self._watch_task
91
+ except asyncio.CancelledError:
92
+ pass
93
+ self._watch_task = None
94
+
95
+ logger.info("Annotation watcher stopped")
96
+
97
+ async def _watch_loop(self) -> None:
98
+ """Main watch loop."""
99
+ while self._running:
100
+ try:
101
+ await self._check_annotations()
102
+ except DoryK8sError as e:
103
+ logger.warning(f"Failed to check annotations: {e}")
104
+ except Exception as e:
105
+ logger.error(f"Unexpected error in annotation watcher: {e}")
106
+
107
+ await asyncio.sleep(self._poll_interval)
108
+
109
+ async def _check_annotations(self) -> None:
110
+ """Check annotations for changes."""
111
+ try:
112
+ annotations = await self._k8s_client.get_pod_annotations(self._pod_name)
113
+ except DoryK8sError:
114
+ # Pod might not exist yet or API unavailable
115
+ return
116
+
117
+ # Check migration annotation
118
+ if self._annotation_changed(self.MIGRATION_ANNOTATION, annotations, "true"):
119
+ logger.info("Migration signal detected")
120
+ if self._on_migration:
121
+ await self._invoke_callback(self._on_migration)
122
+
123
+ # Check shutdown annotation
124
+ if self._annotation_changed(self.SHUTDOWN_ANNOTATION, annotations, "true"):
125
+ logger.info("Shutdown signal detected")
126
+ if self._on_shutdown:
127
+ await self._invoke_callback(self._on_shutdown)
128
+
129
+ # Check snapshot annotation
130
+ if self._annotation_changed(self.SNAPSHOT_ANNOTATION, annotations, "true"):
131
+ logger.info("Snapshot signal detected")
132
+ if self._on_snapshot:
133
+ await self._invoke_callback(self._on_snapshot)
134
+ # Clear snapshot annotation after processing
135
+ await self._clear_annotation(self.SNAPSHOT_ANNOTATION)
136
+
137
+ self._last_annotations = annotations
138
+
139
+ def _annotation_changed(
140
+ self,
141
+ key: str,
142
+ new_annotations: dict[str, str],
143
+ trigger_value: str,
144
+ ) -> bool:
145
+ """Check if annotation changed to trigger value."""
146
+ old_value = self._last_annotations.get(key)
147
+ new_value = new_annotations.get(key)
148
+
149
+ return old_value != new_value and new_value == trigger_value
150
+
151
+ async def _invoke_callback(self, callback: Callable[[], Any]) -> None:
152
+ """Invoke callback, handling async/sync."""
153
+ try:
154
+ if asyncio.iscoroutinefunction(callback):
155
+ await callback()
156
+ else:
157
+ callback()
158
+ except Exception as e:
159
+ logger.error(f"Callback error: {e}")
160
+
161
+ async def _clear_annotation(self, key: str) -> None:
162
+ """Clear an annotation after processing."""
163
+ try:
164
+ await self._k8s_client.patch_pod_annotations(
165
+ self._pod_name,
166
+ {key: None}, # Setting to None removes the annotation
167
+ )
168
+ except DoryK8sError as e:
169
+ logger.warning(f"Failed to clear annotation {key}: {e}")
170
+
171
+ def get_migration_deadline(self) -> float | None:
172
+ """
173
+ Get migration deadline from annotations.
174
+
175
+ Returns:
176
+ Unix timestamp of deadline, or None
177
+ """
178
+ deadline_str = self._last_annotations.get(self.DEADLINE_ANNOTATION)
179
+ if deadline_str:
180
+ try:
181
+ return float(deadline_str)
182
+ except ValueError:
183
+ pass
184
+ return None
dory/k8s/client.py ADDED
@@ -0,0 +1,251 @@
1
+ """
2
+ Kubernetes client wrapper.
3
+
4
+ Provides simplified interface to Kubernetes API.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from dory.utils.errors import DoryK8sError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Optional kubernetes import
15
+ try:
16
+ from kubernetes import client, config
17
+ from kubernetes.client.rest import ApiException
18
+ K8S_AVAILABLE = True
19
+ except ImportError:
20
+ K8S_AVAILABLE = False
21
+ client = None
22
+ config = None
23
+ ApiException = Exception
24
+
25
+
26
+ class K8sClient:
27
+ """
28
+ Kubernetes API client wrapper.
29
+
30
+ Handles configuration loading and provides
31
+ simplified access to common operations.
32
+ """
33
+
34
+ def __init__(self, namespace: str | None = None):
35
+ """
36
+ Initialize Kubernetes client.
37
+
38
+ Args:
39
+ namespace: Kubernetes namespace (auto-detected if not provided)
40
+ """
41
+ self._namespace = namespace
42
+ self._core_api: Any = None
43
+ self._initialized = False
44
+
45
+ def _ensure_initialized(self) -> None:
46
+ """Initialize Kubernetes client if not already done."""
47
+ if self._initialized:
48
+ return
49
+
50
+ if not K8S_AVAILABLE:
51
+ raise DoryK8sError(
52
+ "Kubernetes client not available. "
53
+ "Install with: pip install kubernetes"
54
+ )
55
+
56
+ try:
57
+ # Try in-cluster config first
58
+ config.load_incluster_config()
59
+ logger.debug("Using in-cluster Kubernetes config")
60
+ except config.ConfigException:
61
+ try:
62
+ # Fall back to kubeconfig
63
+ config.load_kube_config()
64
+ logger.debug("Using kubeconfig")
65
+ except config.ConfigException as e:
66
+ raise DoryK8sError(f"Failed to load Kubernetes config: {e}", cause=e)
67
+
68
+ self._core_api = client.CoreV1Api()
69
+
70
+ # Auto-detect namespace if not provided
71
+ if not self._namespace:
72
+ import os
73
+ self._namespace = os.environ.get("POD_NAMESPACE", "default")
74
+
75
+ self._initialized = True
76
+
77
+ @property
78
+ def namespace(self) -> str:
79
+ """Get current namespace."""
80
+ self._ensure_initialized()
81
+ return self._namespace
82
+
83
+ @property
84
+ def core_api(self):
85
+ """Get CoreV1Api client."""
86
+ self._ensure_initialized()
87
+ return self._core_api
88
+
89
+ async def get_pod(self, name: str) -> dict[str, Any]:
90
+ """
91
+ Get pod details.
92
+
93
+ Args:
94
+ name: Pod name
95
+
96
+ Returns:
97
+ Pod details as dictionary
98
+
99
+ Raises:
100
+ DoryK8sError: If operation fails
101
+ """
102
+ self._ensure_initialized()
103
+
104
+ try:
105
+ pod = self._core_api.read_namespaced_pod(
106
+ name=name,
107
+ namespace=self._namespace,
108
+ )
109
+ return pod.to_dict()
110
+
111
+ except ApiException as e:
112
+ if e.status == 404:
113
+ raise DoryK8sError(f"Pod not found: {name}", cause=e)
114
+ raise DoryK8sError(f"Failed to get pod {name}: {e}", cause=e)
115
+
116
+ async def get_pod_annotations(self, name: str) -> dict[str, str]:
117
+ """
118
+ Get pod annotations.
119
+
120
+ Args:
121
+ name: Pod name
122
+
123
+ Returns:
124
+ Annotations dictionary
125
+ """
126
+ pod = await self.get_pod(name)
127
+ return pod.get("metadata", {}).get("annotations", {})
128
+
129
+ async def patch_pod_annotations(
130
+ self,
131
+ name: str,
132
+ annotations: dict[str, str],
133
+ ) -> None:
134
+ """
135
+ Patch pod annotations.
136
+
137
+ Args:
138
+ name: Pod name
139
+ annotations: Annotations to add/update
140
+ """
141
+ self._ensure_initialized()
142
+
143
+ body = {
144
+ "metadata": {
145
+ "annotations": annotations,
146
+ }
147
+ }
148
+
149
+ try:
150
+ self._core_api.patch_namespaced_pod(
151
+ name=name,
152
+ namespace=self._namespace,
153
+ body=body,
154
+ )
155
+ logger.debug(f"Patched annotations on pod {name}")
156
+
157
+ except ApiException as e:
158
+ raise DoryK8sError(f"Failed to patch pod {name}: {e}", cause=e)
159
+
160
+ async def get_configmap(self, name: str) -> dict[str, str] | None:
161
+ """
162
+ Get ConfigMap data.
163
+
164
+ Args:
165
+ name: ConfigMap name
166
+
167
+ Returns:
168
+ ConfigMap data, or None if not found
169
+ """
170
+ self._ensure_initialized()
171
+
172
+ try:
173
+ cm = self._core_api.read_namespaced_config_map(
174
+ name=name,
175
+ namespace=self._namespace,
176
+ )
177
+ return cm.data or {}
178
+
179
+ except ApiException as e:
180
+ if e.status == 404:
181
+ return None
182
+ raise DoryK8sError(f"Failed to get ConfigMap {name}: {e}", cause=e)
183
+
184
+ async def create_or_update_configmap(
185
+ self,
186
+ name: str,
187
+ data: dict[str, str],
188
+ labels: dict[str, str] | None = None,
189
+ ) -> None:
190
+ """
191
+ Create or update a ConfigMap.
192
+
193
+ Args:
194
+ name: ConfigMap name
195
+ data: ConfigMap data
196
+ labels: Optional labels
197
+ """
198
+ self._ensure_initialized()
199
+
200
+ configmap = client.V1ConfigMap(
201
+ metadata=client.V1ObjectMeta(
202
+ name=name,
203
+ namespace=self._namespace,
204
+ labels=labels,
205
+ ),
206
+ data=data,
207
+ )
208
+
209
+ try:
210
+ # Try create first
211
+ self._core_api.create_namespaced_config_map(
212
+ namespace=self._namespace,
213
+ body=configmap,
214
+ )
215
+ logger.debug(f"Created ConfigMap {name}")
216
+
217
+ except ApiException as e:
218
+ if e.status == 409:
219
+ # Already exists, update
220
+ self._core_api.replace_namespaced_config_map(
221
+ name=name,
222
+ namespace=self._namespace,
223
+ body=configmap,
224
+ )
225
+ logger.debug(f"Updated ConfigMap {name}")
226
+ else:
227
+ raise DoryK8sError(f"Failed to create ConfigMap {name}: {e}", cause=e)
228
+
229
+ async def delete_configmap(self, name: str) -> bool:
230
+ """
231
+ Delete a ConfigMap.
232
+
233
+ Args:
234
+ name: ConfigMap name
235
+
236
+ Returns:
237
+ True if deleted, False if not found
238
+ """
239
+ self._ensure_initialized()
240
+
241
+ try:
242
+ self._core_api.delete_namespaced_config_map(
243
+ name=name,
244
+ namespace=self._namespace,
245
+ )
246
+ return True
247
+
248
+ except ApiException as e:
249
+ if e.status == 404:
250
+ return False
251
+ raise DoryK8sError(f"Failed to delete ConfigMap {name}: {e}", cause=e)