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/config/defaults.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Default configuration values for Dory SDK.
|
|
2
|
+
|
|
3
|
+
Only values referenced by DoryConfig schema are included here.
|
|
4
|
+
Component-specific defaults (circuit breakers, OpenTelemetry, etc.)
|
|
5
|
+
live in the components themselves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dory.types import StateBackend
|
|
9
|
+
|
|
10
|
+
# Default configuration dictionary
|
|
11
|
+
DEFAULT_CONFIG = {
|
|
12
|
+
# Lifecycle timeouts
|
|
13
|
+
"startup_timeout_sec": 30,
|
|
14
|
+
"shutdown_timeout_sec": 30,
|
|
15
|
+
|
|
16
|
+
# Health server
|
|
17
|
+
"health_port": 8080,
|
|
18
|
+
|
|
19
|
+
# State management
|
|
20
|
+
"state_backend": StateBackend.CONFIGMAP.value,
|
|
21
|
+
|
|
22
|
+
# Logging
|
|
23
|
+
"log_level": "INFO",
|
|
24
|
+
}
|
dory/config/loader.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loader for Dory SDK.
|
|
3
|
+
|
|
4
|
+
Supports loading from:
|
|
5
|
+
1. YAML configuration file
|
|
6
|
+
2. Environment variables (DORY_ prefix)
|
|
7
|
+
3. Configuration presets
|
|
8
|
+
4. Auto-detection (service name, version, environment)
|
|
9
|
+
5. Default values
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from dory.config.schema import DoryConfig
|
|
22
|
+
from dory.config.presets import get_preset, list_presets, DEVELOPMENT_PRESET
|
|
23
|
+
from dory.utils.errors import DoryConfigError
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigLoader:
|
|
29
|
+
"""
|
|
30
|
+
Smart configuration loader with:
|
|
31
|
+
- Auto-detection of service name/version
|
|
32
|
+
- Preset support (development, production, high-availability)
|
|
33
|
+
- Zero-config mode
|
|
34
|
+
- Progressive disclosure
|
|
35
|
+
|
|
36
|
+
Priority order:
|
|
37
|
+
1. Environment variables (highest)
|
|
38
|
+
2. Config file
|
|
39
|
+
3. Preset (if specified)
|
|
40
|
+
4. Auto-detected defaults
|
|
41
|
+
5. Default values (lowest)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
ENV_PREFIX = "DORY_"
|
|
45
|
+
DEFAULT_CONFIG_PATHS = [
|
|
46
|
+
"/etc/dory/config.yaml",
|
|
47
|
+
"/app/config/dory.yaml",
|
|
48
|
+
"./dory.yaml",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def __init__(self, config_file: str | None = None):
|
|
52
|
+
"""
|
|
53
|
+
Initialize config loader.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config_file: Optional path to YAML config file
|
|
57
|
+
"""
|
|
58
|
+
self._config_file = config_file
|
|
59
|
+
|
|
60
|
+
def load(self) -> DoryConfig:
|
|
61
|
+
"""
|
|
62
|
+
Load and validate configuration.
|
|
63
|
+
|
|
64
|
+
In Kubernetes (under orchestrator):
|
|
65
|
+
All config comes from orchestrator-injected env vars + defaults.
|
|
66
|
+
YAML files and presets are ignored — the orchestrator is the
|
|
67
|
+
single source of truth for health_port, state_backend, log_level,
|
|
68
|
+
timeouts, version, etc.
|
|
69
|
+
|
|
70
|
+
In local development (no orchestrator):
|
|
71
|
+
1. Environment variables (DORY_*)
|
|
72
|
+
2. Config file (dory.yaml)
|
|
73
|
+
3. Preset (if specified)
|
|
74
|
+
4. Auto-detected defaults
|
|
75
|
+
5. Default values
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Validated DoryConfig instance
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
DoryConfigError: If configuration is invalid
|
|
82
|
+
"""
|
|
83
|
+
if self._is_running_in_kubernetes():
|
|
84
|
+
return self._load_orchestrated()
|
|
85
|
+
return self._load_local()
|
|
86
|
+
|
|
87
|
+
def _load_orchestrated(self) -> DoryConfig:
|
|
88
|
+
"""Load config when running under orchestrator in K8s.
|
|
89
|
+
|
|
90
|
+
All settings come from orchestrator-injected env vars.
|
|
91
|
+
YAML files and presets are ignored.
|
|
92
|
+
"""
|
|
93
|
+
# Start with production defaults
|
|
94
|
+
config_dict = get_preset("production")
|
|
95
|
+
|
|
96
|
+
# App info from orchestrator env vars only
|
|
97
|
+
app_config = {
|
|
98
|
+
"name": os.environ.get("PROCESSOR_ID", Path.cwd().name),
|
|
99
|
+
"version": os.environ.get("DORY_APP_VERSION", "0.0.0"),
|
|
100
|
+
"environment": "production",
|
|
101
|
+
}
|
|
102
|
+
config_dict["app"] = app_config
|
|
103
|
+
|
|
104
|
+
# Apply orchestrator-injected env vars (the only override source)
|
|
105
|
+
env_config = self._load_from_env()
|
|
106
|
+
config_dict = self._deep_merge(config_dict, env_config)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
config = DoryConfig(**config_dict)
|
|
110
|
+
logger.info(
|
|
111
|
+
f"Configuration loaded (orchestrated): "
|
|
112
|
+
f"{app_config['name']} v{app_config['version']}"
|
|
113
|
+
)
|
|
114
|
+
logger.debug(f"Full configuration: {config.model_dump()}")
|
|
115
|
+
return config
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise DoryConfigError(f"Invalid configuration: {e}", cause=e)
|
|
118
|
+
|
|
119
|
+
def _load_local(self) -> DoryConfig:
|
|
120
|
+
"""Load config for local development (no orchestrator).
|
|
121
|
+
|
|
122
|
+
Supports YAML files, presets, and auto-detection.
|
|
123
|
+
"""
|
|
124
|
+
# Try to load config file
|
|
125
|
+
file_config = self._load_from_file()
|
|
126
|
+
|
|
127
|
+
# Determine which preset to use
|
|
128
|
+
preset_name = self._determine_preset(file_config)
|
|
129
|
+
|
|
130
|
+
# Start with preset
|
|
131
|
+
if preset_name in list_presets():
|
|
132
|
+
config_dict = get_preset(preset_name)
|
|
133
|
+
logger.info(f"Using configuration preset: {preset_name}")
|
|
134
|
+
else:
|
|
135
|
+
config_dict = DEVELOPMENT_PRESET.copy()
|
|
136
|
+
logger.warning(f"Unknown preset '{preset_name}', using development defaults")
|
|
137
|
+
|
|
138
|
+
# Auto-detect application info
|
|
139
|
+
app_config = self._auto_detect_app_info(
|
|
140
|
+
file_config.get("app", {}) if file_config else {}
|
|
141
|
+
)
|
|
142
|
+
config_dict["app"] = app_config
|
|
143
|
+
|
|
144
|
+
# Deep merge file config (overrides preset), excluding app
|
|
145
|
+
if file_config:
|
|
146
|
+
file_config_without_app = {
|
|
147
|
+
k: v for k, v in file_config.items() if k != "app"
|
|
148
|
+
}
|
|
149
|
+
config_dict = self._deep_merge(config_dict, file_config_without_app)
|
|
150
|
+
|
|
151
|
+
# Apply environment variable overrides (highest priority)
|
|
152
|
+
env_config = self._load_from_env()
|
|
153
|
+
config_dict = self._deep_merge(config_dict, env_config)
|
|
154
|
+
|
|
155
|
+
# Validate and create config object
|
|
156
|
+
try:
|
|
157
|
+
config = DoryConfig(**config_dict)
|
|
158
|
+
logger.info(
|
|
159
|
+
f"Configuration loaded (local): "
|
|
160
|
+
f"{app_config.get('name', 'unknown')} v{app_config.get('version', 'unknown')}"
|
|
161
|
+
)
|
|
162
|
+
logger.debug(f"Full configuration: {config.model_dump()}")
|
|
163
|
+
return config
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise DoryConfigError(f"Invalid configuration: {e}", cause=e)
|
|
166
|
+
|
|
167
|
+
def _load_from_file(self) -> dict[str, Any] | None:
|
|
168
|
+
"""Load configuration from YAML file."""
|
|
169
|
+
config_path = self._find_config_file()
|
|
170
|
+
|
|
171
|
+
if not config_path:
|
|
172
|
+
logger.debug("No config file found, using defaults")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
with open(config_path) as f:
|
|
177
|
+
config = yaml.safe_load(f) or {}
|
|
178
|
+
|
|
179
|
+
logger.info(f"Loaded config from {config_path}")
|
|
180
|
+
return config
|
|
181
|
+
|
|
182
|
+
except yaml.YAMLError as e:
|
|
183
|
+
raise DoryConfigError(f"Invalid YAML in {config_path}: {e}", cause=e)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise DoryConfigError(f"Failed to read {config_path}: {e}", cause=e)
|
|
186
|
+
|
|
187
|
+
def _find_config_file(self) -> Path | None:
|
|
188
|
+
"""Find config file from explicit path or default locations."""
|
|
189
|
+
# Check explicit path first
|
|
190
|
+
if self._config_file:
|
|
191
|
+
path = Path(self._config_file)
|
|
192
|
+
if path.exists():
|
|
193
|
+
return path
|
|
194
|
+
raise DoryConfigError(f"Config file not found: {self._config_file}")
|
|
195
|
+
|
|
196
|
+
# Check environment variable
|
|
197
|
+
env_path = os.environ.get("DORY_CONFIG_FILE")
|
|
198
|
+
if env_path:
|
|
199
|
+
path = Path(env_path)
|
|
200
|
+
if path.exists():
|
|
201
|
+
return path
|
|
202
|
+
logger.warning(f"DORY_CONFIG_FILE set but not found: {env_path}")
|
|
203
|
+
|
|
204
|
+
# Check default locations
|
|
205
|
+
for default_path in self.DEFAULT_CONFIG_PATHS:
|
|
206
|
+
path = Path(default_path)
|
|
207
|
+
if path.exists():
|
|
208
|
+
return path
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def _load_from_env(self) -> dict[str, Any]:
|
|
213
|
+
"""Load configuration from environment variables."""
|
|
214
|
+
config = {}
|
|
215
|
+
|
|
216
|
+
# Map of config keys to environment variable names.
|
|
217
|
+
# Only settings that legitimately vary per deployment are exposed.
|
|
218
|
+
# Internal tuning, system invariants, and hardcoded values are
|
|
219
|
+
# managed by the SDK defaults and should not be overridden.
|
|
220
|
+
env_mapping = {
|
|
221
|
+
# Lifecycle timeouts (vary by app workload)
|
|
222
|
+
"startup_timeout_sec": "DORY_STARTUP_TIMEOUT_SEC",
|
|
223
|
+
"shutdown_timeout_sec": "DORY_SHUTDOWN_TIMEOUT_SEC",
|
|
224
|
+
# Health server
|
|
225
|
+
"health_port": "DORY_HEALTH_PORT",
|
|
226
|
+
# State management (backend choice is per-deployment)
|
|
227
|
+
"state_backend": "DORY_STATE_BACKEND",
|
|
228
|
+
# Logging
|
|
229
|
+
"log_level": "DORY_LOG_LEVEL",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for config_key, env_var in env_mapping.items():
|
|
233
|
+
value = os.environ.get(env_var)
|
|
234
|
+
if value is not None:
|
|
235
|
+
# Convert to appropriate type
|
|
236
|
+
config[config_key] = self._convert_env_value(config_key, value)
|
|
237
|
+
|
|
238
|
+
return config
|
|
239
|
+
|
|
240
|
+
def _convert_env_value(self, key: str, value: str) -> Any:
|
|
241
|
+
"""Convert environment variable string to appropriate type."""
|
|
242
|
+
int_fields = {
|
|
243
|
+
"startup_timeout_sec",
|
|
244
|
+
"shutdown_timeout_sec",
|
|
245
|
+
"health_port",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if key in int_fields:
|
|
249
|
+
try:
|
|
250
|
+
return int(value)
|
|
251
|
+
except ValueError:
|
|
252
|
+
raise DoryConfigError(f"Invalid integer for {key}: {value}")
|
|
253
|
+
|
|
254
|
+
return value
|
|
255
|
+
|
|
256
|
+
def _determine_preset(self, file_config: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
257
|
+
"""
|
|
258
|
+
Determine which preset to use.
|
|
259
|
+
|
|
260
|
+
Priority:
|
|
261
|
+
1. DORY_PRESET environment variable
|
|
262
|
+
2. preset field in config file
|
|
263
|
+
3. Auto-detect based on environment (Kubernetes vs local)
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
file_config: Configuration loaded from file
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Preset name
|
|
270
|
+
"""
|
|
271
|
+
# Check environment variable first
|
|
272
|
+
preset = os.environ.get("DORY_PRESET")
|
|
273
|
+
if preset:
|
|
274
|
+
logger.debug(f"Preset from environment: {preset}")
|
|
275
|
+
return preset
|
|
276
|
+
|
|
277
|
+
# Check config file
|
|
278
|
+
if file_config and "preset" in file_config:
|
|
279
|
+
preset = file_config["preset"]
|
|
280
|
+
logger.debug(f"Preset from config file: {preset}")
|
|
281
|
+
return preset
|
|
282
|
+
|
|
283
|
+
# Auto-detect: if running in Kubernetes, use production preset
|
|
284
|
+
if self._is_running_in_kubernetes():
|
|
285
|
+
logger.info("Kubernetes environment detected, using production preset")
|
|
286
|
+
return "production"
|
|
287
|
+
|
|
288
|
+
# Default to development for local runs
|
|
289
|
+
logger.debug("Local environment detected, using development preset")
|
|
290
|
+
return "development"
|
|
291
|
+
|
|
292
|
+
def _is_running_in_kubernetes(self) -> bool:
|
|
293
|
+
"""
|
|
294
|
+
Detect if running inside a Kubernetes cluster.
|
|
295
|
+
|
|
296
|
+
Checks for:
|
|
297
|
+
- KUBERNETES_SERVICE_HOST (set by K8s)
|
|
298
|
+
- DORY_POD_NAME (set by deployment)
|
|
299
|
+
- /var/run/secrets/kubernetes.io (K8s service account)
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
True if running in Kubernetes
|
|
303
|
+
"""
|
|
304
|
+
# Check K8s service host (always set in K8s pods)
|
|
305
|
+
if os.environ.get("KUBERNETES_SERVICE_HOST"):
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
# Check Dory-specific pod name (set in deployment)
|
|
309
|
+
if os.environ.get("DORY_POD_NAME"):
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
# Check for K8s service account directory
|
|
313
|
+
if Path("/var/run/secrets/kubernetes.io").exists():
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
def _auto_detect_app_info(self, user_app: Dict[str, Any]) -> Dict[str, Any]:
|
|
319
|
+
"""
|
|
320
|
+
Auto-detect service name, version, and environment.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
user_app: User-provided app configuration
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Dictionary with app configuration
|
|
327
|
+
"""
|
|
328
|
+
app = {}
|
|
329
|
+
|
|
330
|
+
# Auto-detect service name
|
|
331
|
+
if "name" in user_app:
|
|
332
|
+
app["name"] = user_app["name"]
|
|
333
|
+
else:
|
|
334
|
+
# Try to detect from directory name
|
|
335
|
+
cwd = Path.cwd()
|
|
336
|
+
app["name"] = cwd.name
|
|
337
|
+
logger.debug(f"Auto-detected service name: {app['name']}")
|
|
338
|
+
|
|
339
|
+
# Version: DORY_APP_VERSION is injected by the orchestrator from DB
|
|
340
|
+
# and always takes precedence. Local auto-detection is only for dev.
|
|
341
|
+
if os.environ.get("DORY_APP_VERSION"):
|
|
342
|
+
app["version"] = os.environ["DORY_APP_VERSION"]
|
|
343
|
+
logger.debug(f"Version from orchestrator: {app['version']}")
|
|
344
|
+
elif "version" in user_app:
|
|
345
|
+
app["version"] = user_app["version"]
|
|
346
|
+
else:
|
|
347
|
+
app["version"] = self._detect_version()
|
|
348
|
+
logger.debug(f"Auto-detected version: {app['version']}")
|
|
349
|
+
|
|
350
|
+
# Auto-detect environment
|
|
351
|
+
if "environment" in user_app:
|
|
352
|
+
app["environment"] = user_app["environment"]
|
|
353
|
+
else:
|
|
354
|
+
app["environment"] = os.environ.get("ENVIRONMENT", "development")
|
|
355
|
+
logger.debug(f"Auto-detected environment: {app['environment']}")
|
|
356
|
+
|
|
357
|
+
# Copy any other user-provided app config
|
|
358
|
+
for key, value in user_app.items():
|
|
359
|
+
if key not in app:
|
|
360
|
+
app[key] = value
|
|
361
|
+
|
|
362
|
+
return app
|
|
363
|
+
|
|
364
|
+
def _detect_version(self) -> str:
|
|
365
|
+
"""
|
|
366
|
+
Try to detect version from various sources.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Version string
|
|
370
|
+
"""
|
|
371
|
+
# Try git tag
|
|
372
|
+
try:
|
|
373
|
+
result = subprocess.run(
|
|
374
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
375
|
+
capture_output=True,
|
|
376
|
+
text=True,
|
|
377
|
+
timeout=1,
|
|
378
|
+
check=False,
|
|
379
|
+
)
|
|
380
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
381
|
+
return result.stdout.strip()
|
|
382
|
+
except Exception:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
# Try pyproject.toml
|
|
386
|
+
pyproject_path = Path("pyproject.toml")
|
|
387
|
+
if pyproject_path.exists():
|
|
388
|
+
try:
|
|
389
|
+
with open(pyproject_path) as f:
|
|
390
|
+
content = f.read()
|
|
391
|
+
# Simple regex to extract version
|
|
392
|
+
match = re.search(r'version\s*=\s*"([^"]+)"', content)
|
|
393
|
+
if match:
|
|
394
|
+
return match.group(1)
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
# Try package.json (for Node.js projects)
|
|
399
|
+
package_json_path = Path("package.json")
|
|
400
|
+
if package_json_path.exists():
|
|
401
|
+
try:
|
|
402
|
+
with open(package_json_path) as f:
|
|
403
|
+
import json
|
|
404
|
+
data = json.load(f)
|
|
405
|
+
if "version" in data:
|
|
406
|
+
return data["version"]
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
# Default
|
|
411
|
+
return "0.1.0-dev"
|
|
412
|
+
|
|
413
|
+
def _deep_merge(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
414
|
+
"""
|
|
415
|
+
Deep merge two dictionaries.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
base: Base dictionary
|
|
419
|
+
override: Dictionary with override values
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Merged dictionary
|
|
423
|
+
"""
|
|
424
|
+
result = base.copy()
|
|
425
|
+
for key, value in override.items():
|
|
426
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
427
|
+
result[key] = self._deep_merge(result[key], value)
|
|
428
|
+
else:
|
|
429
|
+
result[key] = value
|
|
430
|
+
return result
|
dory/config/presets.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Configuration presets for common scenarios.
|
|
2
|
+
|
|
3
|
+
Presets provide sensible defaults for different deployment environments.
|
|
4
|
+
Only flat keys that DoryConfig recognizes are included — nested component
|
|
5
|
+
tuning (circuit breakers, OpenTelemetry, etc.) uses SDK defaults and can
|
|
6
|
+
be customized in code by overriding BaseProcessor.__init__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
|
|
11
|
+
# Development preset - developer-friendly defaults
|
|
12
|
+
DEVELOPMENT_PRESET: Dict[str, Any] = {
|
|
13
|
+
"environment": "development",
|
|
14
|
+
"log_level": "DEBUG",
|
|
15
|
+
"health_port": 0, # Auto-select available port
|
|
16
|
+
"state_backend": "local",
|
|
17
|
+
"startup_timeout_sec": 30,
|
|
18
|
+
"shutdown_timeout_sec": 30,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Production preset - production-ready defaults
|
|
22
|
+
PRODUCTION_PRESET: Dict[str, Any] = {
|
|
23
|
+
"environment": "production",
|
|
24
|
+
"log_level": "INFO",
|
|
25
|
+
"health_port": 8080,
|
|
26
|
+
"state_backend": "configmap",
|
|
27
|
+
"startup_timeout_sec": 120,
|
|
28
|
+
"shutdown_timeout_sec": 30,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# High-availability preset - aggressive fault tolerance
|
|
32
|
+
HIGH_AVAILABILITY_PRESET: Dict[str, Any] = {
|
|
33
|
+
"environment": "production",
|
|
34
|
+
"log_level": "INFO",
|
|
35
|
+
"health_port": 8080,
|
|
36
|
+
"state_backend": "pvc",
|
|
37
|
+
"startup_timeout_sec": 120,
|
|
38
|
+
"shutdown_timeout_sec": 60,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
PRESETS: Dict[str, Dict[str, Any]] = {
|
|
42
|
+
"development": DEVELOPMENT_PRESET,
|
|
43
|
+
"production": PRODUCTION_PRESET,
|
|
44
|
+
"high-availability": HIGH_AVAILABILITY_PRESET,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_preset(name: str) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Get configuration preset by name.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
name: Preset name (development, production, high-availability)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dictionary with preset configuration
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If preset name is unknown
|
|
60
|
+
"""
|
|
61
|
+
if name not in PRESETS:
|
|
62
|
+
raise ValueError(f"Unknown preset: {name}. Available: {list(PRESETS.keys())}")
|
|
63
|
+
return PRESETS[name].copy()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_presets() -> list[str]:
|
|
67
|
+
"""
|
|
68
|
+
Get list of available preset names.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of preset names
|
|
72
|
+
"""
|
|
73
|
+
return list(PRESETS.keys())
|
dory/config/schema.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration schema for Dory SDK.
|
|
3
|
+
|
|
4
|
+
Uses Pydantic for validation and type coercion.
|
|
5
|
+
Only settings that legitimately vary per deployment are configurable.
|
|
6
|
+
|
|
7
|
+
RabbitMQ settings are intentionally absent — the publisher reads
|
|
8
|
+
directly from DORY_RABBITMQ_* environment variables because they
|
|
9
|
+
contain secrets (OAuth2 credentials) that should not flow through
|
|
10
|
+
YAML config files.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field, field_validator
|
|
14
|
+
|
|
15
|
+
from dory.types import StateBackend
|
|
16
|
+
from dory.config.defaults import DEFAULT_CONFIG
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DoryConfig(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Dory SDK configuration schema.
|
|
22
|
+
|
|
23
|
+
Configurable settings can be set via:
|
|
24
|
+
1. YAML config file (dory.yaml)
|
|
25
|
+
2. Environment variables (DORY_ prefix)
|
|
26
|
+
3. Constructor arguments
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Lifecycle timeouts (vary by app workload)
|
|
30
|
+
startup_timeout_sec: int = Field(
|
|
31
|
+
default=DEFAULT_CONFIG["startup_timeout_sec"],
|
|
32
|
+
ge=1,
|
|
33
|
+
le=300,
|
|
34
|
+
description="Maximum time for startup in seconds",
|
|
35
|
+
)
|
|
36
|
+
shutdown_timeout_sec: int = Field(
|
|
37
|
+
default=DEFAULT_CONFIG["shutdown_timeout_sec"],
|
|
38
|
+
ge=1,
|
|
39
|
+
le=300,
|
|
40
|
+
description="Maximum time for shutdown in seconds",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Health server
|
|
44
|
+
health_port: int = Field(
|
|
45
|
+
default=DEFAULT_CONFIG["health_port"],
|
|
46
|
+
ge=0, # 0 = auto-select available port (development mode)
|
|
47
|
+
le=65535,
|
|
48
|
+
description="Port for health/metrics HTTP server (0 = auto-select)",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# State management (backend choice is per-deployment)
|
|
52
|
+
state_backend: str = Field(
|
|
53
|
+
default=DEFAULT_CONFIG["state_backend"],
|
|
54
|
+
description="Backend for state persistence",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Logging
|
|
58
|
+
log_level: str = Field(
|
|
59
|
+
default=DEFAULT_CONFIG["log_level"],
|
|
60
|
+
description="Logging level (DEBUG, INFO, WARNING, ERROR)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@field_validator("state_backend")
|
|
64
|
+
@classmethod
|
|
65
|
+
def validate_state_backend(cls, v: str) -> str:
|
|
66
|
+
"""Validate state backend value."""
|
|
67
|
+
valid_backends = [b.value for b in StateBackend]
|
|
68
|
+
if v not in valid_backends:
|
|
69
|
+
raise ValueError(f"state_backend must be one of {valid_backends}")
|
|
70
|
+
return v
|
|
71
|
+
|
|
72
|
+
@field_validator("log_level")
|
|
73
|
+
@classmethod
|
|
74
|
+
def validate_log_level(cls, v: str) -> str:
|
|
75
|
+
"""Validate log level."""
|
|
76
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
77
|
+
v_upper = v.upper()
|
|
78
|
+
if v_upper not in valid_levels:
|
|
79
|
+
raise ValueError(f"log_level must be one of {valid_levels}")
|
|
80
|
+
return v_upper
|
|
81
|
+
|
|
82
|
+
model_config = {
|
|
83
|
+
"extra": "ignore", # Ignore unknown fields
|
|
84
|
+
}
|
dory/core/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Core modules for Dory SDK."""
|
|
2
|
+
|
|
3
|
+
from dory.core.processor import BaseProcessor
|
|
4
|
+
from dory.core.context import ExecutionContext
|
|
5
|
+
from dory.core.app import DoryApp
|
|
6
|
+
from dory.core.lifecycle import LifecycleManager
|
|
7
|
+
from dory.core.signals import SignalHandler
|
|
8
|
+
from dory.core.modes import (
|
|
9
|
+
ModeManager,
|
|
10
|
+
ProcessingMode,
|
|
11
|
+
ModeTransition,
|
|
12
|
+
ModeTransitionReason,
|
|
13
|
+
ModeConfig,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BaseProcessor",
|
|
18
|
+
"ExecutionContext",
|
|
19
|
+
"DoryApp",
|
|
20
|
+
"LifecycleManager",
|
|
21
|
+
"SignalHandler",
|
|
22
|
+
"ModeManager",
|
|
23
|
+
"ProcessingMode",
|
|
24
|
+
"ModeTransition",
|
|
25
|
+
"ModeTransitionReason",
|
|
26
|
+
"ModeConfig",
|
|
27
|
+
]
|