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.
- dory/__init__.py +101 -0
- dory/auth/__init__.py +10 -0
- dory/auth/oauth2.py +153 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +137 -0
- dory/cli/templates.py +123 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +24 -0
- dory/config/loader.py +430 -0
- dory/config/presets.py +73 -0
- dory/config/schema.py +84 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +434 -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 +564 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/edge/__init__.py +88 -0
- dory/edge/adaptive.py +644 -0
- dory/edge/detector.py +546 -0
- dory/edge/fencing.py +488 -0
- dory/edge/heartbeat.py +598 -0
- dory/edge/role.py +419 -0
- dory/errors/__init__.py +139 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +498 -0
- dory/geo/__init__.py +40 -0
- dory/geo/geolocalizer.py +1034 -0
- dory/health/__init__.py +12 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +635 -0
- dory/k8s/__init__.py +80 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/labels.py +505 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +148 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +46 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +325 -0
- dory/middleware/request_tracker.py +511 -0
- dory/migration/__init__.py +33 -0
- dory/migration/configmap.py +232 -0
- dory/migration/s3_store.py +594 -0
- dory/migration/serialization.py +135 -0
- dory/migration/state_manager.py +286 -0
- dory/migration/transfer.py +382 -0
- dory/monitoring/__init__.py +29 -0
- dory/monitoring/opentelemetry.py +489 -0
- dory/output/__init__.py +31 -0
- dory/output/envelope.py +137 -0
- dory/output/formatter.py +113 -0
- dory/output/rabbitmq.py +632 -0
- dory/output/routing.py +318 -0
- dory/output/validator.py +199 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +487 -0
- dory/recovery/golden_snapshot.py +713 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +482 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +183 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +457 -0
- dory/resilience/retry.py +389 -0
- dory/simple.py +342 -0
- dory/types.py +68 -0
- dory/utils/__init__.py +31 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_processor_sdk-0.0.1.dist-info/METADATA +424 -0
- dory_processor_sdk-0.0.1.dist-info/RECORD +86 -0
- dory_processor_sdk-0.0.1.dist-info/WHEEL +5 -0
- dory_processor_sdk-0.0.1.dist-info/entry_points.txt +2 -0
- dory_processor_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
- 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
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}"
|
dory/auto_instrument.py
ADDED
|
@@ -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
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
|
+
'''
|
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
|
+
]
|