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.
Files changed (69) hide show
  1. dory/__init__.py +70 -0
  2. dory/auto_instrument.py +142 -0
  3. dory/cli/__init__.py +5 -0
  4. dory/cli/main.py +290 -0
  5. dory/cli/templates.py +333 -0
  6. dory/config/__init__.py +23 -0
  7. dory/config/defaults.py +50 -0
  8. dory/config/loader.py +361 -0
  9. dory/config/presets.py +325 -0
  10. dory/config/schema.py +152 -0
  11. dory/core/__init__.py +27 -0
  12. dory/core/app.py +404 -0
  13. dory/core/context.py +209 -0
  14. dory/core/lifecycle.py +214 -0
  15. dory/core/meta.py +121 -0
  16. dory/core/modes.py +479 -0
  17. dory/core/processor.py +654 -0
  18. dory/core/signals.py +122 -0
  19. dory/decorators.py +142 -0
  20. dory/errors/__init__.py +117 -0
  21. dory/errors/classification.py +362 -0
  22. dory/errors/codes.py +495 -0
  23. dory/health/__init__.py +10 -0
  24. dory/health/probes.py +210 -0
  25. dory/health/server.py +306 -0
  26. dory/k8s/__init__.py +11 -0
  27. dory/k8s/annotation_watcher.py +184 -0
  28. dory/k8s/client.py +251 -0
  29. dory/k8s/pod_metadata.py +182 -0
  30. dory/logging/__init__.py +9 -0
  31. dory/logging/logger.py +175 -0
  32. dory/metrics/__init__.py +7 -0
  33. dory/metrics/collector.py +301 -0
  34. dory/middleware/__init__.py +36 -0
  35. dory/middleware/connection_tracker.py +608 -0
  36. dory/middleware/request_id.py +321 -0
  37. dory/middleware/request_tracker.py +501 -0
  38. dory/migration/__init__.py +11 -0
  39. dory/migration/configmap.py +260 -0
  40. dory/migration/serialization.py +167 -0
  41. dory/migration/state_manager.py +301 -0
  42. dory/monitoring/__init__.py +23 -0
  43. dory/monitoring/opentelemetry.py +462 -0
  44. dory/py.typed +2 -0
  45. dory/recovery/__init__.py +60 -0
  46. dory/recovery/golden_image.py +480 -0
  47. dory/recovery/golden_snapshot.py +561 -0
  48. dory/recovery/golden_validator.py +518 -0
  49. dory/recovery/partial_recovery.py +479 -0
  50. dory/recovery/recovery_decision.py +242 -0
  51. dory/recovery/restart_detector.py +142 -0
  52. dory/recovery/state_validator.py +187 -0
  53. dory/resilience/__init__.py +45 -0
  54. dory/resilience/circuit_breaker.py +454 -0
  55. dory/resilience/retry.py +389 -0
  56. dory/sidecar/__init__.py +6 -0
  57. dory/sidecar/main.py +75 -0
  58. dory/sidecar/server.py +329 -0
  59. dory/simple.py +342 -0
  60. dory/types.py +75 -0
  61. dory/utils/__init__.py +25 -0
  62. dory/utils/errors.py +59 -0
  63. dory/utils/retry.py +115 -0
  64. dory/utils/timeout.py +80 -0
  65. dory_sdk-2.1.0.dist-info/METADATA +663 -0
  66. dory_sdk-2.1.0.dist-info/RECORD +69 -0
  67. dory_sdk-2.1.0.dist-info/WHEEL +5 -0
  68. dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
  69. dory_sdk-2.1.0.dist-info/top_level.txt +1 -0
dory/__init__.py ADDED
@@ -0,0 +1,70 @@
1
+ """
2
+ Dory SDK - Python Integration Package for Processor Applications
3
+
4
+ This SDK handles all operational concerns (graceful shutdown, state migration,
5
+ health checks, observability) so developers focus solely on business logic.
6
+
7
+ Quick Start (Class-based API):
8
+ from dory import DoryApp, BaseProcessor, stateful
9
+
10
+ class MyProcessor(BaseProcessor):
11
+ counter = stateful(0) # Auto-saved/restored
12
+
13
+ async def run(self):
14
+ async for _ in self.run_loop(interval=1):
15
+ self.counter += 1
16
+
17
+ if __name__ == '__main__':
18
+ DoryApp().run(MyProcessor)
19
+
20
+ Quick Start (Function-based API):
21
+ from dory.simple import processor, state
22
+
23
+ counter = state(0)
24
+
25
+ @processor
26
+ async def main(ctx):
27
+ async for _ in ctx.run_loop(interval=1):
28
+ counter.value += 1
29
+ """
30
+
31
+ __version__ = "1.0.0"
32
+
33
+ # Core API
34
+ from dory.core.processor import BaseProcessor
35
+ from dory.core.context import ExecutionContext
36
+ from dory.core.app import DoryApp
37
+
38
+ # Configuration
39
+ from dory.config.schema import DoryConfig
40
+
41
+ # Decorators for simplified integration
42
+ from dory.decorators import stateful, StatefulVar
43
+
44
+ # Exceptions
45
+ from dory.utils.errors import (
46
+ DoryError,
47
+ DoryStartupError,
48
+ DoryShutdownError,
49
+ DoryStateError,
50
+ DoryConfigError,
51
+ )
52
+
53
+ __all__ = [
54
+ # Core API
55
+ "BaseProcessor",
56
+ "ExecutionContext",
57
+ "DoryApp",
58
+ "DoryConfig",
59
+ # Decorators
60
+ "stateful",
61
+ "StatefulVar",
62
+ # Exceptions
63
+ "DoryError",
64
+ "DoryStartupError",
65
+ "DoryShutdownError",
66
+ "DoryStateError",
67
+ "DoryConfigError",
68
+ # Version
69
+ "__version__",
70
+ ]
@@ -0,0 +1,142 @@
1
+ """
2
+ Auto-instrumentation decorator for handlers.
3
+
4
+ Automatically applies:
5
+ - Request ID generation
6
+ - Request tracking
7
+ - OpenTelemetry span creation
8
+ - Error classification
9
+ - Attribute injection
10
+
11
+ No manual decorators needed!
12
+ """
13
+
14
+ import functools
15
+ import logging
16
+ from typing import Callable, Any
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def auto_instrument(func: Callable) -> Callable:
22
+ """
23
+ Auto-instrument async function with all SDK features.
24
+
25
+ Automatically handles:
26
+ - Request ID generation
27
+ - Request tracking with timeout
28
+ - OpenTelemetry span creation
29
+ - Span attributes injection
30
+ - Error classification and logging
31
+
32
+ Usage:
33
+ @auto_instrument
34
+ async def handler(self, request):
35
+ # All instrumentation is automatic!
36
+ return {"status": "ok"}
37
+
38
+ Or with metaclass (no decorator needed):
39
+ class MyProcessor(BaseProcessor):
40
+ # All handle_* methods automatically instrumented!
41
+ async def handle_request(self, request):
42
+ return {"status": "ok"}
43
+
44
+ Args:
45
+ func: Async function to instrument
46
+
47
+ Returns:
48
+ Instrumented async function
49
+ """
50
+
51
+ @functools.wraps(func)
52
+ async def wrapper(self, *args, **kwargs):
53
+ # Extract request if present (first arg or keyword arg)
54
+ request = None
55
+ if args:
56
+ request = args[0]
57
+ elif "request" in kwargs:
58
+ request = kwargs["request"]
59
+
60
+ # Get processor components (auto-initialized by BaseProcessor)
61
+ request_id_middleware = getattr(self, "request_id_middleware", None)
62
+ request_tracker = getattr(self, "request_tracker", None)
63
+ otel = getattr(self, "otel", None)
64
+ error_classifier = getattr(self, "error_classifier", None)
65
+
66
+ # 1. Generate request ID
67
+ request_id = None
68
+ if request_id_middleware:
69
+ request_id = request_id_middleware.generate_id()
70
+ # Store in request for retrieval
71
+ if request is not None and hasattr(request, "__setitem__"):
72
+ request["request_id"] = request_id
73
+ elif request is not None and hasattr(request, "__dict__"):
74
+ request.request_id = request_id
75
+ logger.debug(f"Generated request ID: {request_id}")
76
+
77
+ # 2. Track request
78
+ request_tracker_ctx = None
79
+ if request_tracker:
80
+ request_tracker_ctx = request_tracker.track_request(request_id)
81
+ await request_tracker_ctx.__aenter__()
82
+ logger.debug(f"Started tracking request: {request_id}")
83
+
84
+ # 3. Create OpenTelemetry span
85
+ span_ctx = None
86
+ if otel:
87
+ span_name = f"{self.__class__.__name__}.{func.__name__}"
88
+ attributes = {
89
+ "function": func.__name__,
90
+ "class": self.__class__.__name__,
91
+ }
92
+ if request_id:
93
+ attributes["request.id"] = request_id
94
+ if request is not None and hasattr(request, "path"):
95
+ attributes["endpoint"] = str(request.path)
96
+ if request is not None and hasattr(request, "method"):
97
+ attributes["http.method"] = str(request.method)
98
+
99
+ span_ctx = otel.create_span(span_name, attributes=attributes)
100
+ span_ctx.__enter__()
101
+ logger.debug(f"Created span: {span_name}")
102
+
103
+ try:
104
+ # Execute handler
105
+ result = await func(self, *args, **kwargs)
106
+
107
+ # Mark request as successful
108
+ if request_tracker_ctx:
109
+ await request_tracker_ctx.__aexit__(None, None, None)
110
+ logger.debug(f"Request completed successfully: {request_id}")
111
+
112
+ if span_ctx:
113
+ span_ctx.__exit__(None, None, None)
114
+
115
+ return result
116
+
117
+ except Exception as e:
118
+ # Classify error
119
+ if error_classifier:
120
+ error_info = error_classifier.classify(e)
121
+ logger.warning(
122
+ f"Handler error: {error_info.error_type.value} - {e}",
123
+ extra={
124
+ "request_id": request_id,
125
+ "error_type": error_info.error_type.value,
126
+ "is_transient": error_info.is_transient,
127
+ },
128
+ )
129
+ else:
130
+ logger.warning(f"Handler error: {e}", extra={"request_id": request_id})
131
+
132
+ # Mark request as failed
133
+ if request_tracker_ctx:
134
+ await request_tracker_ctx.__aexit__(type(e), e, None)
135
+
136
+ # Record exception in span
137
+ if span_ctx:
138
+ span_ctx.__exit__(type(e), e, None)
139
+
140
+ raise
141
+
142
+ return wrapper
dory/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Dory SDK CLI tools."""
2
+
3
+ from dory.cli.main import main
4
+
5
+ __all__ = ["main"]
dory/cli/main.py ADDED
@@ -0,0 +1,290 @@
1
+ """
2
+ Dory SDK CLI - Command line tools for Dory SDK.
3
+
4
+ Provides commands for:
5
+ - Generating Kubernetes manifests (RBAC, Deployment, etc.)
6
+ - Initializing new Dory projects
7
+ - Validating configuration
8
+
9
+ Usage:
10
+ dory init my-app --image my-app:latest
11
+ dory generate k8s --name my-app --image my-app:latest
12
+ dory validate
13
+ """
14
+
15
+ import argparse
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from dory.cli.templates import (
21
+ generate_rbac,
22
+ generate_deployment,
23
+ generate_pod,
24
+ generate_all,
25
+ generate_dockerfile,
26
+ generate_processor_template,
27
+ )
28
+
29
+
30
+ def cmd_init(args: argparse.Namespace) -> int:
31
+ """Initialize a new Dory project."""
32
+ name = args.name
33
+ output_dir = Path(args.output or ".")
34
+
35
+ print(f"Initializing Dory project: {name}")
36
+
37
+ # Create directories
38
+ k8s_dir = output_dir / "k8s"
39
+ k8s_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ # Generate files
42
+ files = {
43
+ "main.py": generate_processor_template(name),
44
+ "Dockerfile": generate_dockerfile(name),
45
+ "k8s/rbac.yaml": generate_rbac(name, args.namespace),
46
+ "k8s/deployment.yaml": generate_deployment(
47
+ name=name,
48
+ image=args.image or f"{name}:latest",
49
+ namespace=args.namespace,
50
+ replicas=args.replicas,
51
+ health_port=args.health_port,
52
+ app_port=args.app_port,
53
+ ),
54
+ }
55
+
56
+ for filename, content in files.items():
57
+ filepath = output_dir / filename
58
+ filepath.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ if filepath.exists() and not args.force:
61
+ print(f" Skipping {filename} (already exists, use --force to overwrite)")
62
+ continue
63
+
64
+ filepath.write_text(content)
65
+ print(f" Created {filename}")
66
+
67
+ print()
68
+ print("Next steps:")
69
+ print(f" 1. Edit main.py with your processor logic")
70
+ print(f" 2. Build: docker build -t {name}:latest .")
71
+ print(f" 3. Deploy: kubectl apply -f k8s/")
72
+ print()
73
+
74
+ return 0
75
+
76
+
77
+ def cmd_generate(args: argparse.Namespace) -> int:
78
+ """Generate Kubernetes manifests."""
79
+ output_dir = Path(args.output or "k8s")
80
+ output_dir.mkdir(parents=True, exist_ok=True)
81
+
82
+ name = args.name
83
+ image = args.image or f"{name}:latest"
84
+ namespace = args.namespace
85
+
86
+ if args.type == "rbac":
87
+ content = generate_rbac(name, namespace)
88
+ filename = "rbac.yaml"
89
+ elif args.type == "deployment":
90
+ content = generate_deployment(
91
+ name=name,
92
+ image=image,
93
+ namespace=namespace,
94
+ replicas=args.replicas,
95
+ health_port=args.health_port,
96
+ app_port=args.app_port,
97
+ )
98
+ filename = "deployment.yaml"
99
+ elif args.type == "pod":
100
+ content = generate_pod(
101
+ name=name,
102
+ image=image,
103
+ namespace=namespace,
104
+ health_port=args.health_port,
105
+ app_port=args.app_port,
106
+ )
107
+ filename = "pod.yaml"
108
+ elif args.type == "all":
109
+ content = generate_all(
110
+ name=name,
111
+ image=image,
112
+ namespace=namespace,
113
+ replicas=args.replicas,
114
+ health_port=args.health_port,
115
+ app_port=args.app_port,
116
+ )
117
+ filename = "all.yaml"
118
+ else:
119
+ print(f"Unknown type: {args.type}")
120
+ return 1
121
+
122
+ filepath = output_dir / filename
123
+
124
+ if filepath.exists() and not args.force:
125
+ print(f"File {filepath} already exists. Use --force to overwrite.")
126
+ return 1
127
+
128
+ filepath.write_text(content)
129
+ print(f"Generated: {filepath}")
130
+
131
+ return 0
132
+
133
+
134
+ def cmd_validate(args: argparse.Namespace) -> int:
135
+ """Validate Dory configuration."""
136
+ from dory.config.loader import ConfigLoader
137
+
138
+ config_file = args.config
139
+
140
+ try:
141
+ loader = ConfigLoader(config_file=config_file)
142
+ config = loader.load()
143
+ print("Configuration is valid!")
144
+ print()
145
+ print("Current settings:")
146
+ for key, value in config.model_dump().items():
147
+ print(f" {key}: {value}")
148
+ return 0
149
+ except Exception as e:
150
+ print(f"Configuration error: {e}")
151
+ return 1
152
+
153
+
154
+ def main(argv: list[str] | None = None) -> int:
155
+ """Main CLI entry point."""
156
+ parser = argparse.ArgumentParser(
157
+ prog="dory",
158
+ description="Dory SDK CLI - Tools for building stateful Kubernetes processors",
159
+ )
160
+ parser.add_argument(
161
+ "--version",
162
+ action="version",
163
+ version="%(prog)s 1.0.0",
164
+ )
165
+
166
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
167
+
168
+ # init command
169
+ init_parser = subparsers.add_parser(
170
+ "init",
171
+ help="Initialize a new Dory project",
172
+ )
173
+ init_parser.add_argument(
174
+ "name",
175
+ help="Project/app name",
176
+ )
177
+ init_parser.add_argument(
178
+ "-o", "--output",
179
+ help="Output directory (default: current directory)",
180
+ )
181
+ init_parser.add_argument(
182
+ "-i", "--image",
183
+ help="Docker image name (default: <name>:latest)",
184
+ )
185
+ init_parser.add_argument(
186
+ "-n", "--namespace",
187
+ default="default",
188
+ help="Kubernetes namespace (default: default)",
189
+ )
190
+ init_parser.add_argument(
191
+ "--replicas",
192
+ type=int,
193
+ default=1,
194
+ help="Number of replicas (default: 1)",
195
+ )
196
+ init_parser.add_argument(
197
+ "--health-port",
198
+ type=int,
199
+ default=8080,
200
+ help="Health server port (default: 8080)",
201
+ )
202
+ init_parser.add_argument(
203
+ "--app-port",
204
+ type=int,
205
+ default=8081,
206
+ help="Application port (default: 8081)",
207
+ )
208
+ init_parser.add_argument(
209
+ "-f", "--force",
210
+ action="store_true",
211
+ help="Overwrite existing files",
212
+ )
213
+ init_parser.set_defaults(func=cmd_init)
214
+
215
+ # generate command
216
+ gen_parser = subparsers.add_parser(
217
+ "generate",
218
+ help="Generate Kubernetes manifests",
219
+ )
220
+ gen_parser.add_argument(
221
+ "type",
222
+ choices=["rbac", "deployment", "pod", "all"],
223
+ help="Type of manifest to generate",
224
+ )
225
+ gen_parser.add_argument(
226
+ "-n", "--name",
227
+ required=True,
228
+ help="Application name",
229
+ )
230
+ gen_parser.add_argument(
231
+ "-i", "--image",
232
+ help="Docker image (default: <name>:latest)",
233
+ )
234
+ gen_parser.add_argument(
235
+ "--namespace",
236
+ default="default",
237
+ help="Kubernetes namespace (default: default)",
238
+ )
239
+ gen_parser.add_argument(
240
+ "--replicas",
241
+ type=int,
242
+ default=1,
243
+ help="Number of replicas (default: 1)",
244
+ )
245
+ gen_parser.add_argument(
246
+ "--health-port",
247
+ type=int,
248
+ default=8080,
249
+ help="Health server port (default: 8080)",
250
+ )
251
+ gen_parser.add_argument(
252
+ "--app-port",
253
+ type=int,
254
+ default=8081,
255
+ help="Application port (default: 8081)",
256
+ )
257
+ gen_parser.add_argument(
258
+ "-o", "--output",
259
+ default="k8s",
260
+ help="Output directory (default: k8s)",
261
+ )
262
+ gen_parser.add_argument(
263
+ "-f", "--force",
264
+ action="store_true",
265
+ help="Overwrite existing files",
266
+ )
267
+ gen_parser.set_defaults(func=cmd_generate)
268
+
269
+ # validate command
270
+ validate_parser = subparsers.add_parser(
271
+ "validate",
272
+ help="Validate Dory configuration",
273
+ )
274
+ validate_parser.add_argument(
275
+ "-c", "--config",
276
+ help="Path to config file (default: dory.yaml)",
277
+ )
278
+ validate_parser.set_defaults(func=cmd_validate)
279
+
280
+ args = parser.parse_args(argv)
281
+
282
+ if not args.command:
283
+ parser.print_help()
284
+ return 0
285
+
286
+ return args.func(args)
287
+
288
+
289
+ if __name__ == "__main__":
290
+ sys.exit(main())