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
@@ -0,0 +1,135 @@
1
+ """
2
+ State serialization utilities.
3
+
4
+ Handles JSON serialization/deserialization with checksum validation.
5
+ """
6
+
7
+ import hashlib
8
+ import json
9
+ import time
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ from dory.utils.errors import DoryStateError
14
+
15
+
16
+ @dataclass
17
+ class StateEnvelope:
18
+ """
19
+ Envelope wrapping state data with metadata.
20
+
21
+ Attributes:
22
+ payload: The actual state data
23
+ metadata: Metadata about when/where state was created
24
+ checksum: SHA256 checksum of payload for integrity
25
+ """
26
+
27
+ payload: dict[str, Any]
28
+ metadata: dict[str, Any]
29
+ checksum: str
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ """Convert to dictionary for serialization."""
33
+ return {
34
+ "payload": self.payload,
35
+ "metadata": self.metadata,
36
+ "checksum": self.checksum,
37
+ }
38
+
39
+ @classmethod
40
+ def from_dict(cls, data: dict[str, Any]) -> "StateEnvelope":
41
+ """Create from dictionary."""
42
+ return cls(
43
+ payload=data["payload"],
44
+ metadata=data["metadata"],
45
+ checksum=data["checksum"],
46
+ )
47
+
48
+
49
+ class StateSerializer:
50
+ """
51
+ Serializes and deserializes state with integrity checking.
52
+
53
+ Uses JSON format with SHA256 checksums for integrity validation.
54
+ """
55
+
56
+ @staticmethod
57
+ def compute_checksum(payload: dict[str, Any]) -> str:
58
+ """
59
+ Compute SHA256 checksum for payload.
60
+
61
+ Args:
62
+ payload: State payload
63
+
64
+ Returns:
65
+ Hex-encoded SHA256 checksum
66
+ """
67
+ payload_json = json.dumps(payload, sort_keys=True)
68
+ return hashlib.sha256(payload_json.encode()).hexdigest()
69
+
70
+ def serialize(
71
+ self,
72
+ state: dict[str, Any],
73
+ processor_id: str,
74
+ pod_name: str,
75
+ restart_count: int = 0,
76
+ ) -> str:
77
+ """
78
+ Serialize state to JSON string with envelope.
79
+
80
+ Args:
81
+ state: State dictionary to serialize
82
+ processor_id: Processor ID for metadata
83
+ pod_name: Pod name for metadata
84
+ restart_count: Current restart count
85
+
86
+ Returns:
87
+ JSON string with state envelope
88
+ """
89
+ envelope = StateEnvelope(
90
+ payload=state,
91
+ metadata={
92
+ "timestamp": time.time(),
93
+ "timestamp_iso": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
94
+ "processor_id": processor_id,
95
+ "pod_name": pod_name,
96
+ "restart_count": restart_count,
97
+ },
98
+ checksum=self.compute_checksum(state),
99
+ )
100
+
101
+ return json.dumps(envelope.to_dict(), indent=2)
102
+
103
+ def deserialize(self, data: str) -> dict[str, Any]:
104
+ """
105
+ Deserialize state from JSON string.
106
+
107
+ Args:
108
+ data: JSON string with state envelope
109
+
110
+ Returns:
111
+ State payload dictionary
112
+
113
+ Raises:
114
+ DoryStateError: If deserialization or validation fails
115
+ """
116
+ try:
117
+ envelope_dict = json.loads(data)
118
+ except json.JSONDecodeError as e:
119
+ raise DoryStateError(f"Invalid JSON in state data: {e}", cause=e)
120
+
121
+ try:
122
+ envelope = StateEnvelope.from_dict(envelope_dict)
123
+ except KeyError as e:
124
+ raise DoryStateError(f"Missing field in state envelope: {e}", cause=e)
125
+
126
+ # Validate checksum
127
+ expected_checksum = self.compute_checksum(envelope.payload)
128
+ if envelope.checksum != expected_checksum:
129
+ raise DoryStateError(
130
+ f"State checksum mismatch: expected {expected_checksum}, "
131
+ f"got {envelope.checksum}"
132
+ )
133
+
134
+ return envelope.payload
135
+
@@ -0,0 +1,286 @@
1
+ """
2
+ StateManager - High-level state management for migrations.
3
+
4
+ Provides unified interface for state operations across different
5
+ storage backends (ConfigMap, S3, local file).
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ from dory.types import StateBackend
14
+ from dory.migration.serialization import StateSerializer
15
+ from dory.migration.configmap import ConfigMapStore
16
+ from dory.migration.s3_store import S3Store, S3Config
17
+ from dory.utils.errors import DoryStateError
18
+
19
+ if TYPE_CHECKING:
20
+ from dory.config.schema import DoryConfig
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class StateManager:
26
+ """
27
+ High-level state management for processor migrations.
28
+
29
+ Supports multiple backends:
30
+ - ConfigMap: Kubernetes ConfigMap (default, <1MB)
31
+ - S3: AWS S3 (for multi-cluster)
32
+ - Local/PVC: Local file (tries /data first, falls back to /tmp)
33
+ """
34
+
35
+ LOCAL_STATE_PATH = "/data/dory-state.json"
36
+ LOCAL_STATE_PATH_FALLBACK = "/tmp/dory-state.json"
37
+
38
+ def __init__(
39
+ self,
40
+ backend: str | StateBackend = StateBackend.CONFIGMAP,
41
+ config: "DoryConfig | None" = None,
42
+ ):
43
+ """
44
+ Initialize state manager.
45
+
46
+ Args:
47
+ backend: Storage backend to use
48
+ config: SDK configuration
49
+ """
50
+ if isinstance(backend, str):
51
+ backend = StateBackend(backend)
52
+
53
+ self._backend = backend
54
+ self._config = config
55
+ self._serializer = StateSerializer()
56
+ self._configmap_store: ConfigMapStore | None = None
57
+ self._s3_store: S3Store | None = None
58
+
59
+ # Get namespace from environment
60
+ self._namespace = os.environ.get("POD_NAMESPACE", "default")
61
+ self._pod_name = os.environ.get("POD_NAME", "unknown")
62
+
63
+ async def save_state(
64
+ self,
65
+ processor_id: str,
66
+ state: dict[str, Any],
67
+ restart_count: int = 0,
68
+ ) -> None:
69
+ """
70
+ Save processor state.
71
+
72
+ Args:
73
+ processor_id: Processor ID
74
+ state: State dictionary to save
75
+ restart_count: Current restart count for metadata
76
+
77
+ Raises:
78
+ DoryStateError: If save fails
79
+ """
80
+ logger.debug(f"Saving state for processor {processor_id}")
81
+
82
+ # Serialize state
83
+ state_json = self._serializer.serialize(
84
+ state=state,
85
+ processor_id=processor_id,
86
+ pod_name=self._pod_name,
87
+ restart_count=restart_count,
88
+ )
89
+
90
+ # Save to backend (PVC uses the same local file path as Local)
91
+ if self._backend == StateBackend.CONFIGMAP:
92
+ await self._save_to_configmap(processor_id, state_json)
93
+ elif self._backend in (StateBackend.LOCAL, StateBackend.PVC):
94
+ await self._save_to_local(processor_id, state_json)
95
+ elif self._backend == StateBackend.S3:
96
+ await self._save_to_s3(processor_id, state_json)
97
+ else:
98
+ raise DoryStateError(f"Unsupported state backend: {self._backend}")
99
+
100
+ logger.info(f"State saved for processor {processor_id}")
101
+
102
+ async def load_state(self, processor_id: str) -> dict[str, Any] | None:
103
+ """
104
+ Load processor state.
105
+
106
+ Args:
107
+ processor_id: Processor ID
108
+
109
+ Returns:
110
+ State dictionary, or None if no state found
111
+
112
+ Raises:
113
+ DoryStateError: If load fails
114
+ """
115
+ logger.debug(f"Loading state for processor {processor_id}")
116
+
117
+ # Load from backend
118
+ state_json: str | None = None
119
+
120
+ if self._backend == StateBackend.CONFIGMAP:
121
+ state_json = await self._load_from_configmap(processor_id)
122
+ elif self._backend in (StateBackend.LOCAL, StateBackend.PVC):
123
+ state_json = await self._load_from_local(processor_id)
124
+ elif self._backend == StateBackend.S3:
125
+ state_json = await self._load_from_s3(processor_id)
126
+ else:
127
+ raise DoryStateError(f"Unsupported state backend: {self._backend}")
128
+
129
+ if state_json is None:
130
+ logger.debug(f"No state found for processor {processor_id}")
131
+ return None
132
+
133
+ # Deserialize state
134
+ state = self._serializer.deserialize(state_json)
135
+ logger.info(f"State loaded for processor {processor_id}")
136
+ return state
137
+
138
+ async def delete_state(self, processor_id: str) -> bool:
139
+ """
140
+ Delete processor state (golden image reset).
141
+
142
+ Args:
143
+ processor_id: Processor ID
144
+
145
+ Returns:
146
+ True if state was deleted
147
+
148
+ Raises:
149
+ DoryStateError: If delete fails
150
+ """
151
+ logger.debug(f"Deleting state for processor {processor_id}")
152
+
153
+ if self._backend == StateBackend.CONFIGMAP:
154
+ return await self._delete_from_configmap(processor_id)
155
+ elif self._backend in (StateBackend.LOCAL, StateBackend.PVC):
156
+ return await self._delete_from_local(processor_id)
157
+ elif self._backend == StateBackend.S3:
158
+ return await self._delete_from_s3(processor_id)
159
+ else:
160
+ raise DoryStateError(f"Unsupported state backend: {self._backend}")
161
+
162
+ # =========================================================================
163
+ # ConfigMap Backend
164
+ # =========================================================================
165
+
166
+ def _get_configmap_store(self) -> ConfigMapStore:
167
+ """Get or create ConfigMap store instance."""
168
+ if self._configmap_store is None:
169
+ self._configmap_store = ConfigMapStore(namespace=self._namespace)
170
+ return self._configmap_store
171
+
172
+ async def _save_to_configmap(self, processor_id: str, state_json: str) -> None:
173
+ """Save state to Kubernetes ConfigMap."""
174
+ await self._get_configmap_store().save(processor_id, state_json)
175
+
176
+ async def _load_from_configmap(self, processor_id: str) -> str | None:
177
+ """Load state from Kubernetes ConfigMap."""
178
+ return await self._get_configmap_store().load(processor_id)
179
+
180
+ async def _delete_from_configmap(self, processor_id: str) -> bool:
181
+ """Delete state ConfigMap."""
182
+ return await self._get_configmap_store().delete(processor_id)
183
+
184
+ # =========================================================================
185
+ # Local File Backend (also used by PVC — tries /data first, then /tmp)
186
+ # =========================================================================
187
+
188
+ def _get_local_path(self, processor_id: str) -> Path:
189
+ """Get local file path for state."""
190
+ # Try /data first (works when PVC is mounted), fall back to /tmp
191
+ base_path = Path(self.LOCAL_STATE_PATH).parent
192
+ if not base_path.exists():
193
+ base_path = Path(self.LOCAL_STATE_PATH_FALLBACK).parent
194
+
195
+ return base_path / f"dory-state-{processor_id}.json"
196
+
197
+ async def _save_to_local(self, processor_id: str, state_json: str) -> None:
198
+ """Save state to local file."""
199
+ path = self._get_local_path(processor_id)
200
+ path.parent.mkdir(parents=True, exist_ok=True)
201
+
202
+ try:
203
+ path.write_text(state_json)
204
+ logger.debug(f"State saved to local file: {path}")
205
+ except Exception as e:
206
+ raise DoryStateError(f"Failed to save state to {path}: {e}", cause=e)
207
+
208
+ async def _load_from_local(self, processor_id: str) -> str | None:
209
+ """Load state from local file."""
210
+ path = self._get_local_path(processor_id)
211
+
212
+ if not path.exists():
213
+ return None
214
+
215
+ try:
216
+ return path.read_text()
217
+ except Exception as e:
218
+ raise DoryStateError(f"Failed to load state from {path}: {e}", cause=e)
219
+
220
+ async def _delete_from_local(self, processor_id: str) -> bool:
221
+ """Delete local state file."""
222
+ path = self._get_local_path(processor_id)
223
+
224
+ if not path.exists():
225
+ return False
226
+
227
+ try:
228
+ path.unlink()
229
+ logger.debug(f"State file deleted: {path}")
230
+ return True
231
+ except Exception as e:
232
+ raise DoryStateError(f"Failed to delete state file {path}: {e}", cause=e)
233
+
234
+ # =========================================================================
235
+ # S3 Backend
236
+ # =========================================================================
237
+
238
+ def _get_s3_store(self) -> S3Store:
239
+ """Get or create S3 store instance."""
240
+ if self._s3_store is None:
241
+ # Try to get S3 config from DoryConfig if available
242
+ s3_config = None
243
+ if self._config and hasattr(self._config, "s3_config"):
244
+ s3_config = self._config.s3_config
245
+
246
+ self._s3_store = S3Store(config=s3_config)
247
+
248
+ return self._s3_store
249
+
250
+ async def _save_to_s3(self, processor_id: str, state_json: str) -> None:
251
+ """Save state to S3 with offline buffering support."""
252
+ store = self._get_s3_store()
253
+ await store.save(
254
+ processor_id,
255
+ state_json,
256
+ metadata={
257
+ "pod-name": self._pod_name,
258
+ "namespace": self._namespace,
259
+ },
260
+ )
261
+
262
+ async def _load_from_s3(self, processor_id: str) -> str | None:
263
+ """Load state from S3 (falls back to local buffer if unavailable)."""
264
+ store = self._get_s3_store()
265
+ return await store.load(processor_id)
266
+
267
+ async def _delete_from_s3(self, processor_id: str) -> bool:
268
+ """Delete state from S3."""
269
+ store = self._get_s3_store()
270
+ return await store.delete(processor_id)
271
+
272
+ async def sync_s3_buffer(self) -> int:
273
+ """
274
+ Sync locally buffered states to S3.
275
+
276
+ Call this periodically on edge nodes to upload states
277
+ that were buffered during connectivity issues.
278
+
279
+ Returns:
280
+ Number of states synced
281
+ """
282
+ if self._backend != StateBackend.S3:
283
+ return 0
284
+
285
+ store = self._get_s3_store()
286
+ return await store.sync_buffer()