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
@@ -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
+ ]