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/__init__.py ADDED
@@ -0,0 +1,101 @@
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__ = "0.0.1"
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
+ # Edge node support (fencing, split-brain prevention, heartbeat)
54
+ from dory.edge import (
55
+ FencingManager,
56
+ FencingConfig,
57
+ FencingToken,
58
+ FencingError,
59
+ FenceViolation,
60
+ StaleEpochError,
61
+ ProcessorRole,
62
+ RoleManager,
63
+ RoleTransition,
64
+ HeartbeatManager,
65
+ HeartbeatConfig,
66
+ ConnectivityStatus,
67
+ EdgeHealthReporter,
68
+ )
69
+
70
+ __all__ = [
71
+ # Core API
72
+ "BaseProcessor",
73
+ "ExecutionContext",
74
+ "DoryApp",
75
+ "DoryConfig",
76
+ # Decorators
77
+ "stateful",
78
+ "StatefulVar",
79
+ # Exceptions
80
+ "DoryError",
81
+ "DoryStartupError",
82
+ "DoryShutdownError",
83
+ "DoryStateError",
84
+ "DoryConfigError",
85
+ # Edge node support
86
+ "FencingManager",
87
+ "FencingConfig",
88
+ "FencingToken",
89
+ "FencingError",
90
+ "FenceViolation",
91
+ "StaleEpochError",
92
+ "ProcessorRole",
93
+ "RoleManager",
94
+ "RoleTransition",
95
+ "HeartbeatManager",
96
+ "HeartbeatConfig",
97
+ "ConnectivityStatus",
98
+ "EdgeHealthReporter",
99
+ # Version
100
+ "__version__",
101
+ ]
dory/auth/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Authentication providers for Dory SDK."""
2
+
3
+ from dory.auth.oauth2 import OAuth2Error, OAuth2TokenProvider, TokenProvider
4
+
5
+
6
+ __all__ = [
7
+ "OAuth2Error",
8
+ "OAuth2TokenProvider",
9
+ "TokenProvider",
10
+ ]
dory/auth/oauth2.py ADDED
@@ -0,0 +1,153 @@
1
+ """OAuth2 client credentials token provider with caching and auto-refresh."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Protocol, runtime_checkable
6
+ from urllib.parse import quote
7
+
8
+ import aiohttp
9
+
10
+ from dory.utils.errors import DoryConfigError
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OAuth2Error(Exception):
17
+ """Raised when an OAuth2 token request fails."""
18
+
19
+ def __init__(self, message: str, status_code: int | None = None):
20
+ super().__init__(message)
21
+ self.message = message
22
+ self.status_code = status_code
23
+
24
+
25
+ @runtime_checkable
26
+ class TokenProvider(Protocol):
27
+ """Protocol for token providers.
28
+
29
+ Implement this protocol to provide custom token sources
30
+ (e.g., Vault, AWS IAM) without being locked to OAuth2.
31
+ """
32
+
33
+ async def get_token(self) -> str: ...
34
+
35
+
36
+ class OAuth2TokenProvider:
37
+ """Async OAuth2 client credentials token provider with caching and auto-refresh.
38
+
39
+ Fetches tokens from an OAuth2 token endpoint using the client credentials
40
+ grant type. Tokens are cached in memory and automatically refreshed before
41
+ expiry based on the configured buffer.
42
+
43
+ Args:
44
+ token_url: OAuth2 token endpoint URL.
45
+ client_id: OAuth2 client ID.
46
+ client_secret: OAuth2 client secret.
47
+ scopes: List of OAuth2 scopes to request.
48
+ refresh_buffer_sec: Seconds before expiry to trigger a refresh.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ token_url: str,
54
+ client_id: str,
55
+ client_secret: str,
56
+ scopes: list[str] | None = None,
57
+ refresh_buffer_sec: int = 60,
58
+ ):
59
+ if not token_url:
60
+ raise DoryConfigError("OAuth2 token_url is required")
61
+ if not client_id:
62
+ raise DoryConfigError("OAuth2 client_id is required")
63
+ if not client_secret:
64
+ raise DoryConfigError("OAuth2 client_secret is required")
65
+
66
+ self._token_url = token_url
67
+ self._client_id = client_id
68
+ self._client_secret = client_secret
69
+ self._scopes = scopes or []
70
+ self._refresh_buffer_sec = refresh_buffer_sec
71
+
72
+ self._cached_token: str | None = None
73
+ self._token_expiry: float = 0.0
74
+
75
+ async def get_token(self) -> str:
76
+ """Return a valid access token, fetching or refreshing as needed.
77
+
78
+ Returns:
79
+ A valid OAuth2 access token string.
80
+
81
+ Raises:
82
+ OAuth2Error: If the token request fails.
83
+ """
84
+ now = time.monotonic()
85
+ if self._cached_token and now < (self._token_expiry - self._refresh_buffer_sec):
86
+ return self._cached_token
87
+
88
+ return await self._fetch_token()
89
+
90
+ async def _fetch_token(self) -> str:
91
+ """Fetch a new token from the OAuth2 token endpoint."""
92
+ data: dict[str, str] = {
93
+ "grant_type": "client_credentials",
94
+ }
95
+ if self._scopes:
96
+ data["scope"] = " ".join(self._scopes)
97
+
98
+ # Use HTTP Basic Auth (RFC 6749 §2.3.1) as required by Cognito
99
+ auth = aiohttp.BasicAuth(self._client_id, self._client_secret)
100
+
101
+ try:
102
+ async with aiohttp.ClientSession() as session, session.post(self._token_url, data=data, auth=auth) as resp:
103
+ if resp.status != 200:
104
+ body = await resp.text()
105
+ raise OAuth2Error(
106
+ f"Token request failed (HTTP {resp.status}): {body}",
107
+ status_code=resp.status,
108
+ )
109
+
110
+ result = await resp.json()
111
+ except OAuth2Error:
112
+ raise
113
+ except Exception as e:
114
+ raise OAuth2Error(f"Token request failed: {e}") from e
115
+
116
+ access_token = result.get("access_token")
117
+ if not access_token:
118
+ raise OAuth2Error("Token response missing 'access_token' field")
119
+
120
+ expires_in = result.get("expires_in", 3600)
121
+ self._cached_token = str(access_token)
122
+ self._token_expiry = time.monotonic() + expires_in
123
+
124
+ logger.info(f"OAuth2 token acquired, expires in {expires_in}s")
125
+ return self._cached_token
126
+
127
+ async def build_amqp_url(
128
+ self,
129
+ host: str,
130
+ port: int = 5671,
131
+ vhost: str = "/",
132
+ tls: bool = True,
133
+ ) -> str:
134
+ """Build an AMQP URL using the current token as password.
135
+
136
+ Args:
137
+ host: RabbitMQ hostname.
138
+ port: RabbitMQ port (default 5671 for TLS).
139
+ vhost: RabbitMQ virtual host.
140
+ tls: Whether to use amqps:// (TLS) or amqp://.
141
+
142
+ Returns:
143
+ A fully-formed AMQP connection URL.
144
+ """
145
+ token = await self.get_token()
146
+ scheme = "amqps" if tls else "amqp"
147
+ encoded_token = quote(token, safe="")
148
+ # Use empty username with token as password (RabbitMQ OAuth2 convention)
149
+ if vhost == "/":
150
+ vhost_path = "%2f"
151
+ else:
152
+ vhost_path = quote(vhost, safe="")
153
+ return f"{scheme}://:{encoded_token}@{host}:{port}/{vhost_path}"
@@ -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,137 @@
1
+ """
2
+ Dory SDK CLI - Command line tools for Dory SDK.
3
+
4
+ Provides commands for:
5
+ - Initializing new Dory projects
6
+ - Validating configuration
7
+
8
+ Usage:
9
+ dory init my-app
10
+ dory validate
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from dory import __version__
18
+ from dory.cli.templates import (
19
+ generate_dockerfile,
20
+ generate_processor_template,
21
+ )
22
+
23
+
24
+ def cmd_init(args: argparse.Namespace) -> int:
25
+ """Initialize a new Dory project."""
26
+ name = args.name
27
+ output_dir = Path(args.output or ".")
28
+
29
+ print(f"Initializing Dory project: {name}")
30
+
31
+ # Generate files
32
+ files = {
33
+ "main.py": generate_processor_template(name),
34
+ "Dockerfile": generate_dockerfile(name),
35
+ }
36
+
37
+ for filename, content in files.items():
38
+ filepath = output_dir / filename
39
+ filepath.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ if filepath.exists() and not args.force:
42
+ print(f" Skipping {filename} (already exists, use --force to overwrite)")
43
+ continue
44
+
45
+ filepath.write_text(content)
46
+ print(f" Created {filename}")
47
+
48
+ print()
49
+ print("Next steps:")
50
+ print(f" 1. Edit main.py with your processor logic")
51
+ print(f" 2. Build: docker build -t {name}:latest .")
52
+ print(f" 3. Register the processor in the orchestrator database")
53
+ print()
54
+
55
+ return 0
56
+
57
+
58
+ def cmd_validate(args: argparse.Namespace) -> int:
59
+ """Validate Dory configuration."""
60
+ from dory.config.loader import ConfigLoader
61
+
62
+ config_file = args.config
63
+
64
+ try:
65
+ loader = ConfigLoader(config_file=config_file)
66
+ config = loader.load()
67
+ print("Configuration is valid!")
68
+ print()
69
+ print("Current settings:")
70
+ for key, value in config.model_dump().items():
71
+ print(f" {key}: {value}")
72
+ return 0
73
+ except Exception as e:
74
+ print(f"Configuration error: {e}")
75
+ return 1
76
+
77
+
78
+ def main(argv: list[str] | None = None) -> int:
79
+ """Main CLI entry point."""
80
+ parser = argparse.ArgumentParser(
81
+ prog="dory",
82
+ description="Dory SDK CLI - Tools for building stateful Kubernetes processors",
83
+ )
84
+ parser.add_argument(
85
+ "--version",
86
+ action="version",
87
+ version=f"%(prog)s {__version__}",
88
+ )
89
+
90
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
91
+
92
+ # init command
93
+ init_parser = subparsers.add_parser(
94
+ "init",
95
+ help="Initialize a new Dory project",
96
+ )
97
+ init_parser.add_argument(
98
+ "name",
99
+ help="Project/app name",
100
+ )
101
+ init_parser.add_argument(
102
+ "-o", "--output",
103
+ help="Output directory (default: current directory)",
104
+ )
105
+ init_parser.add_argument(
106
+ "-i", "--image",
107
+ help="Docker image name (default: <name>:latest)",
108
+ )
109
+ init_parser.add_argument(
110
+ "-f", "--force",
111
+ action="store_true",
112
+ help="Overwrite existing files",
113
+ )
114
+ init_parser.set_defaults(func=cmd_init)
115
+
116
+ # validate command
117
+ validate_parser = subparsers.add_parser(
118
+ "validate",
119
+ help="Validate Dory configuration",
120
+ )
121
+ validate_parser.add_argument(
122
+ "-c", "--config",
123
+ help="Path to config file (default: dory.yaml)",
124
+ )
125
+ validate_parser.set_defaults(func=cmd_validate)
126
+
127
+ args = parser.parse_args(argv)
128
+
129
+ if not args.command:
130
+ parser.print_help()
131
+ return 0
132
+
133
+ return args.func(args)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
dory/cli/templates.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ Templates for generating project files.
3
+ """
4
+
5
+
6
+ def generate_dockerfile(name: str) -> str:
7
+ """Generate Dockerfile for a Dory processor."""
8
+ return f'''# Dockerfile for Dory processor: {name}
9
+
10
+ FROM python:3.11-slim
11
+
12
+ WORKDIR /app
13
+
14
+ # Install system dependencies
15
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
16
+ curl \\
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # Install Dory SDK for EKS deployment
20
+ RUN pip install --no-cache-dir dory-processor-sdk[production]
21
+
22
+ # Copy application
23
+ COPY main.py .
24
+
25
+ # Expose ports (8080 for health, 8081 for app)
26
+ EXPOSE 8080 8081
27
+
28
+ # Health check
29
+ HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \\
30
+ CMD curl -f http://localhost:8080/health || exit 1
31
+
32
+ # Run the application
33
+ CMD ["python", "main.py"]
34
+ '''
35
+
36
+
37
+ def generate_processor_template(name: str) -> str:
38
+ """Generate a template processor Python file."""
39
+ class_name = "".join(word.capitalize() for word in name.replace("-", "_").split("_"))
40
+
41
+ return f'''"""
42
+ {name} - A Dory-powered stateful processor.
43
+
44
+ Features:
45
+ - Zero-downtime migration
46
+ - Automatic state persistence
47
+ - Graceful shutdown
48
+ - Health monitoring
49
+
50
+ Generated by: dory init {name}
51
+ """
52
+
53
+ import asyncio
54
+ from dory import DoryApp, BaseProcessor, stateful
55
+
56
+
57
+ class {class_name}(BaseProcessor):
58
+ """
59
+ Your processor implementation.
60
+
61
+ The @stateful decorator automatically handles state save/restore.
62
+ Just implement the run() method with your business logic.
63
+ """
64
+
65
+ # Stateful variables (automatically saved and restored)
66
+ counter = stateful(0)
67
+ data = stateful(dict)
68
+
69
+ async def startup(self) -> None:
70
+ """Initialize resources (optional)."""
71
+ self.context.logger().info("Starting up...")
72
+ # Load models, open connections, etc.
73
+
74
+ async def run(self) -> None:
75
+ """Main processing loop."""
76
+ logger = self.context.logger()
77
+
78
+ # Use run_loop() for automatic shutdown detection
79
+ async for i in self.run_loop(interval=1):
80
+ self.counter += 1
81
+ logger.info(f"Iteration {{i}}: counter={{self.counter}}")
82
+
83
+ # Your business logic here
84
+ # ...
85
+
86
+ async def shutdown(self) -> None:
87
+ """Cleanup resources (optional)."""
88
+ self.context.logger().info(f"Shutting down. Final counter: {{self.counter}}")
89
+ # Close connections, flush buffers, etc.
90
+
91
+
92
+ if __name__ == "__main__":
93
+ DoryApp().run({class_name})
94
+ '''
95
+
96
+
97
+ def generate_simple_processor_template(name: str) -> str:
98
+ """Generate a minimal processor using function-based API."""
99
+ return f'''"""
100
+ {name} - A minimal Dory processor using function-based API.
101
+
102
+ Generated by: dory init {name} --simple
103
+ """
104
+
105
+ from dory.simple import processor, state
106
+
107
+ # State variables (automatically saved and restored)
108
+ counter = state(0)
109
+ data = state(dict)
110
+
111
+
112
+ @processor
113
+ async def main(ctx):
114
+ """Main processing loop."""
115
+ logger = ctx.logger()
116
+
117
+ async for i in ctx.run_loop(interval=1):
118
+ counter.value += 1
119
+ logger.info(f"Iteration {{i}}: counter={{counter.value}}")
120
+
121
+ # Your business logic here
122
+ # ...
123
+ '''
@@ -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
+ ]