dory-sdk 2.1.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.
- dory/__init__.py +70 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +290 -0
- dory/cli/templates.py +333 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +50 -0
- dory/config/loader.py +361 -0
- dory/config/presets.py +325 -0
- dory/config/schema.py +152 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +404 -0
- dory/core/context.py +209 -0
- dory/core/lifecycle.py +214 -0
- dory/core/meta.py +121 -0
- dory/core/modes.py +479 -0
- dory/core/processor.py +654 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/errors/__init__.py +117 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +495 -0
- dory/health/__init__.py +10 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +306 -0
- dory/k8s/__init__.py +11 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +175 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +36 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +321 -0
- dory/middleware/request_tracker.py +501 -0
- dory/migration/__init__.py +11 -0
- dory/migration/configmap.py +260 -0
- dory/migration/serialization.py +167 -0
- dory/migration/state_manager.py +301 -0
- dory/monitoring/__init__.py +23 -0
- dory/monitoring/opentelemetry.py +462 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +480 -0
- dory/recovery/golden_snapshot.py +561 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +479 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +187 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +454 -0
- dory/resilience/retry.py +389 -0
- dory/sidecar/__init__.py +6 -0
- dory/sidecar/main.py +75 -0
- dory/sidecar/server.py +329 -0
- dory/simple.py +342 -0
- dory/types.py +75 -0
- dory/utils/__init__.py +25 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_sdk-2.1.0.dist-info/METADATA +663 -0
- dory_sdk-2.1.0.dist-info/RECORD +69 -0
- dory_sdk-2.1.0.dist-info/WHEEL +5 -0
- dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
- dory_sdk-2.1.0.dist-info/top_level.txt +1 -0
dory/cli/templates.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Templates for generating Kubernetes manifests and project files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_rbac(name: str, namespace: str = "default") -> str:
|
|
7
|
+
"""Generate RBAC manifest (ServiceAccount, Role, RoleBinding)."""
|
|
8
|
+
return f'''# RBAC for Dory processor: {name}
|
|
9
|
+
# Allows the processor to manage ConfigMaps for state persistence
|
|
10
|
+
|
|
11
|
+
apiVersion: v1
|
|
12
|
+
kind: ServiceAccount
|
|
13
|
+
metadata:
|
|
14
|
+
name: {name}
|
|
15
|
+
namespace: {namespace}
|
|
16
|
+
---
|
|
17
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
18
|
+
kind: Role
|
|
19
|
+
metadata:
|
|
20
|
+
name: {name}-state-manager
|
|
21
|
+
namespace: {namespace}
|
|
22
|
+
rules:
|
|
23
|
+
- apiGroups: [""]
|
|
24
|
+
resources: ["configmaps"]
|
|
25
|
+
verbs: ["get", "create", "update", "patch", "delete"]
|
|
26
|
+
---
|
|
27
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
28
|
+
kind: RoleBinding
|
|
29
|
+
metadata:
|
|
30
|
+
name: {name}-state-manager
|
|
31
|
+
namespace: {namespace}
|
|
32
|
+
subjects:
|
|
33
|
+
- kind: ServiceAccount
|
|
34
|
+
name: {name}
|
|
35
|
+
namespace: {namespace}
|
|
36
|
+
roleRef:
|
|
37
|
+
kind: Role
|
|
38
|
+
name: {name}-state-manager
|
|
39
|
+
apiGroup: rbac.authorization.k8s.io
|
|
40
|
+
'''
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generate_deployment(
|
|
44
|
+
name: str,
|
|
45
|
+
image: str,
|
|
46
|
+
namespace: str = "default",
|
|
47
|
+
replicas: int = 1,
|
|
48
|
+
health_port: int = 8080,
|
|
49
|
+
app_port: int = 8081,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Generate Deployment manifest with probes and PreStop hook."""
|
|
52
|
+
return f'''# Deployment for Dory processor: {name}
|
|
53
|
+
|
|
54
|
+
apiVersion: apps/v1
|
|
55
|
+
kind: Deployment
|
|
56
|
+
metadata:
|
|
57
|
+
name: {name}
|
|
58
|
+
namespace: {namespace}
|
|
59
|
+
labels:
|
|
60
|
+
app: {name}
|
|
61
|
+
spec:
|
|
62
|
+
replicas: {replicas}
|
|
63
|
+
selector:
|
|
64
|
+
matchLabels:
|
|
65
|
+
app: {name}
|
|
66
|
+
template:
|
|
67
|
+
metadata:
|
|
68
|
+
labels:
|
|
69
|
+
app: {name}
|
|
70
|
+
spec:
|
|
71
|
+
serviceAccountName: {name}
|
|
72
|
+
terminationGracePeriodSeconds: 35
|
|
73
|
+
|
|
74
|
+
containers:
|
|
75
|
+
- name: {name}
|
|
76
|
+
image: {image}
|
|
77
|
+
ports:
|
|
78
|
+
- name: health
|
|
79
|
+
containerPort: {health_port}
|
|
80
|
+
- name: app
|
|
81
|
+
containerPort: {app_port}
|
|
82
|
+
|
|
83
|
+
env:
|
|
84
|
+
# Pod metadata (required by Dory SDK)
|
|
85
|
+
- name: DORY_POD_NAME
|
|
86
|
+
valueFrom:
|
|
87
|
+
fieldRef:
|
|
88
|
+
fieldPath: metadata.name
|
|
89
|
+
- name: DORY_POD_NAMESPACE
|
|
90
|
+
valueFrom:
|
|
91
|
+
fieldRef:
|
|
92
|
+
fieldPath: metadata.namespace
|
|
93
|
+
- name: NODE_NAME
|
|
94
|
+
valueFrom:
|
|
95
|
+
fieldRef:
|
|
96
|
+
fieldPath: spec.nodeName
|
|
97
|
+
|
|
98
|
+
# Liveness probe - is the process alive?
|
|
99
|
+
livenessProbe:
|
|
100
|
+
httpGet:
|
|
101
|
+
path: /healthz
|
|
102
|
+
port: {health_port}
|
|
103
|
+
initialDelaySeconds: 10
|
|
104
|
+
periodSeconds: 10
|
|
105
|
+
timeoutSeconds: 5
|
|
106
|
+
failureThreshold: 3
|
|
107
|
+
|
|
108
|
+
# Readiness probe - ready for traffic?
|
|
109
|
+
readinessProbe:
|
|
110
|
+
httpGet:
|
|
111
|
+
path: /ready
|
|
112
|
+
port: {health_port}
|
|
113
|
+
initialDelaySeconds: 5
|
|
114
|
+
periodSeconds: 5
|
|
115
|
+
timeoutSeconds: 3
|
|
116
|
+
failureThreshold: 3
|
|
117
|
+
|
|
118
|
+
# PreStop hook - save state before shutdown
|
|
119
|
+
lifecycle:
|
|
120
|
+
preStop:
|
|
121
|
+
httpGet:
|
|
122
|
+
path: /prestop
|
|
123
|
+
port: {health_port}
|
|
124
|
+
|
|
125
|
+
resources:
|
|
126
|
+
requests:
|
|
127
|
+
memory: "256Mi"
|
|
128
|
+
cpu: "100m"
|
|
129
|
+
limits:
|
|
130
|
+
memory: "512Mi"
|
|
131
|
+
cpu: "500m"
|
|
132
|
+
'''
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def generate_pod(
|
|
136
|
+
name: str,
|
|
137
|
+
image: str,
|
|
138
|
+
namespace: str = "default",
|
|
139
|
+
health_port: int = 8080,
|
|
140
|
+
app_port: int = 8081,
|
|
141
|
+
) -> str:
|
|
142
|
+
"""Generate standalone Pod manifest (for testing)."""
|
|
143
|
+
return f'''# Pod for Dory processor: {name}
|
|
144
|
+
# Use this for testing. For production, use Deployment.
|
|
145
|
+
|
|
146
|
+
apiVersion: v1
|
|
147
|
+
kind: Pod
|
|
148
|
+
metadata:
|
|
149
|
+
name: {name}
|
|
150
|
+
namespace: {namespace}
|
|
151
|
+
labels:
|
|
152
|
+
app: {name}
|
|
153
|
+
spec:
|
|
154
|
+
serviceAccountName: {name}
|
|
155
|
+
terminationGracePeriodSeconds: 35
|
|
156
|
+
|
|
157
|
+
containers:
|
|
158
|
+
- name: {name}
|
|
159
|
+
image: {image}
|
|
160
|
+
ports:
|
|
161
|
+
- name: health
|
|
162
|
+
containerPort: {health_port}
|
|
163
|
+
- name: app
|
|
164
|
+
containerPort: {app_port}
|
|
165
|
+
|
|
166
|
+
env:
|
|
167
|
+
- name: DORY_POD_NAME
|
|
168
|
+
valueFrom:
|
|
169
|
+
fieldRef:
|
|
170
|
+
fieldPath: metadata.name
|
|
171
|
+
- name: DORY_POD_NAMESPACE
|
|
172
|
+
valueFrom:
|
|
173
|
+
fieldRef:
|
|
174
|
+
fieldPath: metadata.namespace
|
|
175
|
+
- name: NODE_NAME
|
|
176
|
+
valueFrom:
|
|
177
|
+
fieldRef:
|
|
178
|
+
fieldPath: spec.nodeName
|
|
179
|
+
|
|
180
|
+
livenessProbe:
|
|
181
|
+
httpGet:
|
|
182
|
+
path: /healthz
|
|
183
|
+
port: {health_port}
|
|
184
|
+
initialDelaySeconds: 10
|
|
185
|
+
periodSeconds: 10
|
|
186
|
+
|
|
187
|
+
readinessProbe:
|
|
188
|
+
httpGet:
|
|
189
|
+
path: /ready
|
|
190
|
+
port: {health_port}
|
|
191
|
+
initialDelaySeconds: 5
|
|
192
|
+
periodSeconds: 5
|
|
193
|
+
|
|
194
|
+
lifecycle:
|
|
195
|
+
preStop:
|
|
196
|
+
httpGet:
|
|
197
|
+
path: /prestop
|
|
198
|
+
port: {health_port}
|
|
199
|
+
'''
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def generate_all(
|
|
203
|
+
name: str,
|
|
204
|
+
image: str,
|
|
205
|
+
namespace: str = "default",
|
|
206
|
+
replicas: int = 1,
|
|
207
|
+
health_port: int = 8080,
|
|
208
|
+
app_port: int = 8081,
|
|
209
|
+
) -> str:
|
|
210
|
+
"""Generate all manifests in a single file."""
|
|
211
|
+
rbac = generate_rbac(name, namespace)
|
|
212
|
+
deployment = generate_deployment(name, image, namespace, replicas, health_port, app_port)
|
|
213
|
+
return f"{rbac}---\n{deployment}"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def generate_dockerfile(name: str) -> str:
|
|
217
|
+
"""Generate Dockerfile for a Dory processor."""
|
|
218
|
+
return f'''# Dockerfile for Dory processor: {name}
|
|
219
|
+
|
|
220
|
+
FROM python:3.11-slim
|
|
221
|
+
|
|
222
|
+
WORKDIR /app
|
|
223
|
+
|
|
224
|
+
# Install system dependencies
|
|
225
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
226
|
+
curl \\
|
|
227
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
228
|
+
|
|
229
|
+
# Install Dory SDK with Kubernetes support
|
|
230
|
+
RUN pip install --no-cache-dir dory-sdk[kubernetes]
|
|
231
|
+
|
|
232
|
+
# Copy application
|
|
233
|
+
COPY main.py .
|
|
234
|
+
|
|
235
|
+
# Expose ports (8080 for health, 8081 for app)
|
|
236
|
+
EXPOSE 8080 8081
|
|
237
|
+
|
|
238
|
+
# Health check
|
|
239
|
+
HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \\
|
|
240
|
+
CMD curl -f http://localhost:8080/healthz || exit 1
|
|
241
|
+
|
|
242
|
+
# Run the application
|
|
243
|
+
CMD ["python", "main.py"]
|
|
244
|
+
'''
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def generate_processor_template(name: str) -> str:
|
|
248
|
+
"""Generate a template processor Python file."""
|
|
249
|
+
class_name = "".join(word.capitalize() for word in name.replace("-", "_").split("_"))
|
|
250
|
+
|
|
251
|
+
return f'''"""
|
|
252
|
+
{name} - A Dory-powered stateful processor.
|
|
253
|
+
|
|
254
|
+
Features:
|
|
255
|
+
- Zero-downtime migration
|
|
256
|
+
- Automatic state persistence
|
|
257
|
+
- Graceful shutdown
|
|
258
|
+
- Health monitoring
|
|
259
|
+
|
|
260
|
+
Generated by: dory init {name}
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
import asyncio
|
|
264
|
+
from dory import DoryApp, BaseProcessor, stateful
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class {class_name}(BaseProcessor):
|
|
268
|
+
"""
|
|
269
|
+
Your processor implementation.
|
|
270
|
+
|
|
271
|
+
The @stateful decorator automatically handles state save/restore.
|
|
272
|
+
Just implement the run() method with your business logic.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
# Stateful variables (automatically saved and restored)
|
|
276
|
+
counter = stateful(0)
|
|
277
|
+
data = stateful(dict)
|
|
278
|
+
|
|
279
|
+
async def startup(self) -> None:
|
|
280
|
+
"""Initialize resources (optional)."""
|
|
281
|
+
self.context.logger().info("Starting up...")
|
|
282
|
+
# Load models, open connections, etc.
|
|
283
|
+
|
|
284
|
+
async def run(self) -> None:
|
|
285
|
+
"""Main processing loop."""
|
|
286
|
+
logger = self.context.logger()
|
|
287
|
+
|
|
288
|
+
# Use run_loop() for automatic shutdown detection
|
|
289
|
+
async for i in self.run_loop(interval=1):
|
|
290
|
+
self.counter += 1
|
|
291
|
+
logger.info(f"Iteration {{i}}: counter={{self.counter}}")
|
|
292
|
+
|
|
293
|
+
# Your business logic here
|
|
294
|
+
# ...
|
|
295
|
+
|
|
296
|
+
async def shutdown(self) -> None:
|
|
297
|
+
"""Cleanup resources (optional)."""
|
|
298
|
+
self.context.logger().info(f"Shutting down. Final counter: {{self.counter}}")
|
|
299
|
+
# Close connections, flush buffers, etc.
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
DoryApp().run({class_name})
|
|
304
|
+
'''
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def generate_simple_processor_template(name: str) -> str:
|
|
308
|
+
"""Generate a minimal processor using function-based API."""
|
|
309
|
+
return f'''"""
|
|
310
|
+
{name} - A minimal Dory processor using function-based API.
|
|
311
|
+
|
|
312
|
+
Generated by: dory init {name} --simple
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
from dory.simple import processor, state
|
|
316
|
+
|
|
317
|
+
# State variables (automatically saved and restored)
|
|
318
|
+
counter = state(0)
|
|
319
|
+
data = state(dict)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@processor
|
|
323
|
+
async def main(ctx):
|
|
324
|
+
"""Main processing loop."""
|
|
325
|
+
logger = ctx.logger()
|
|
326
|
+
|
|
327
|
+
async for i in ctx.run_loop(interval=1):
|
|
328
|
+
counter.value += 1
|
|
329
|
+
logger.info(f"Iteration {{i}}: counter={{counter.value}}")
|
|
330
|
+
|
|
331
|
+
# Your business logic here
|
|
332
|
+
# ...
|
|
333
|
+
'''
|
dory/config/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Configuration loading and schema definitions."""
|
|
2
|
+
|
|
3
|
+
from dory.config.schema import DoryConfig
|
|
4
|
+
from dory.config.loader import ConfigLoader
|
|
5
|
+
from dory.config.defaults import DEFAULT_CONFIG
|
|
6
|
+
from dory.config.presets import (
|
|
7
|
+
get_preset,
|
|
8
|
+
list_presets,
|
|
9
|
+
DEVELOPMENT_PRESET,
|
|
10
|
+
PRODUCTION_PRESET,
|
|
11
|
+
HIGH_AVAILABILITY_PRESET,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DoryConfig",
|
|
16
|
+
"ConfigLoader",
|
|
17
|
+
"DEFAULT_CONFIG",
|
|
18
|
+
"get_preset",
|
|
19
|
+
"list_presets",
|
|
20
|
+
"DEVELOPMENT_PRESET",
|
|
21
|
+
"PRODUCTION_PRESET",
|
|
22
|
+
"HIGH_AVAILABILITY_PRESET",
|
|
23
|
+
]
|
dory/config/defaults.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Default configuration values for Dory SDK."""
|
|
2
|
+
|
|
3
|
+
from dory.types import StateBackend, LogFormat
|
|
4
|
+
|
|
5
|
+
# Default configuration dictionary
|
|
6
|
+
DEFAULT_CONFIG = {
|
|
7
|
+
# Lifecycle timeouts
|
|
8
|
+
"startup_timeout_sec": 30,
|
|
9
|
+
"shutdown_timeout_sec": 30,
|
|
10
|
+
"health_check_interval_sec": 10,
|
|
11
|
+
|
|
12
|
+
# Health server
|
|
13
|
+
"health_port": 8080,
|
|
14
|
+
"health_path": "/healthz",
|
|
15
|
+
"ready_path": "/ready",
|
|
16
|
+
"metrics_path": "/metrics",
|
|
17
|
+
|
|
18
|
+
# State management
|
|
19
|
+
"state_backend": StateBackend.CONFIGMAP.value,
|
|
20
|
+
"state_pvc_mount": "/data",
|
|
21
|
+
"state_s3_bucket": None,
|
|
22
|
+
"state_s3_prefix": "dory-state",
|
|
23
|
+
|
|
24
|
+
# Recovery
|
|
25
|
+
"max_restart_attempts": 3,
|
|
26
|
+
"restart_backoff_sec": 10,
|
|
27
|
+
"golden_image_threshold": 3,
|
|
28
|
+
|
|
29
|
+
# Logging
|
|
30
|
+
"log_level": "INFO",
|
|
31
|
+
"log_format": LogFormat.JSON.value,
|
|
32
|
+
|
|
33
|
+
# Metrics
|
|
34
|
+
"metrics_enabled": True,
|
|
35
|
+
"metrics_prefix": "dory",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_default(key: str, default=None):
|
|
40
|
+
"""
|
|
41
|
+
Get a default configuration value.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
key: Configuration key
|
|
45
|
+
default: Value to return if key not found
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Default value for the key
|
|
49
|
+
"""
|
|
50
|
+
return DEFAULT_CONFIG.get(key, default)
|