Samara 0.2__tar.gz → 0.4__tar.gz

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 (78) hide show
  1. {samara-0.2 → samara-0.4}/PKG-INFO +2 -4
  2. {samara-0.2 → samara-0.4}/README.md +0 -2
  3. {samara-0.2 → samara-0.4}/pyproject.toml +2 -2
  4. {samara-0.2 → samara-0.4}/src/samara/__init__.py +4 -3
  5. {samara-0.2 → samara-0.4}/src/samara/cli.py +18 -138
  6. {samara-0.2 → samara-0.4}/src/samara/exceptions.py +14 -33
  7. {samara-0.2 → samara-0.4}/src/samara/settings.py +21 -2
  8. {samara-0.2 → samara-0.4}/src/samara/utils/file.py +2 -36
  9. {samara-0.2 → samara-0.4}/src/samara/utils/http.py +13 -13
  10. {samara-0.2 → samara-0.4}/src/samara/utils/logger.py +20 -8
  11. {samara-0.2 → samara-0.4}/src/samara/workflow/actions/base.py +7 -4
  12. {samara-0.2 → samara-0.4}/src/samara/workflow/actions/http.py +9 -4
  13. {samara-0.2 → samara-0.4}/src/samara/workflow/controller.py +0 -1
  14. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/hooks.py +28 -8
  15. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_extract.py +2 -2
  16. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_load.py +5 -3
  17. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_transform.py +0 -1
  18. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/extract.py +21 -48
  19. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/job.py +9 -10
  20. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/load.py +22 -49
  21. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/schema.py +19 -14
  22. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/session.py +54 -5
  23. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transform.py +13 -40
  24. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/base.py +8 -8
  25. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/filter.py +1 -7
  26. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/join.py +4 -23
  27. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/select.py +0 -5
  28. samara-0.2/src/samara/alert/__init__.py +0 -16
  29. samara-0.2/src/samara/alert/channels/__init__.py +0 -30
  30. samara-0.2/src/samara/alert/channels/base.py +0 -124
  31. samara-0.2/src/samara/alert/channels/email.py +0 -135
  32. samara-0.2/src/samara/alert/channels/file.py +0 -101
  33. samara-0.2/src/samara/alert/channels/http.py +0 -146
  34. samara-0.2/src/samara/alert/controller.py +0 -219
  35. samara-0.2/src/samara/alert/rules/__init__.py +0 -28
  36. samara-0.2/src/samara/alert/rules/base.py +0 -86
  37. samara-0.2/src/samara/alert/rules/env_vars_matches.py +0 -120
  38. samara-0.2/src/samara/alert/rules/exception_regex.py +0 -94
  39. samara-0.2/src/samara/alert/template.py +0 -103
  40. samara-0.2/src/samara/alert/trigger.py +0 -155
  41. {samara-0.2 → samara-0.4}/LICENSE +0 -0
  42. {samara-0.2 → samara-0.4}/src/samara/__main__.py +0 -0
  43. {samara-0.2 → samara-0.4}/src/samara/telemetry.py +0 -0
  44. {samara-0.2 → samara-0.4}/src/samara/types.py +0 -0
  45. {samara-0.2 → samara-0.4}/src/samara/utils/__init__.py +0 -0
  46. {samara-0.2 → samara-0.4}/src/samara/workflow/__init__.py +0 -0
  47. {samara-0.2 → samara-0.4}/src/samara/workflow/actions/__init__.py +0 -0
  48. {samara-0.2 → samara-0.4}/src/samara/workflow/actions/move_or_copy_job_files.py +0 -0
  49. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/__init__.py +0 -0
  50. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/__init__.py +0 -0
  51. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_job.py +0 -0
  52. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/__init__.py +0 -0
  53. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_aggregate.py +0 -0
  54. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_cast.py +0 -0
  55. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_distinct.py +0 -0
  56. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_drop.py +0 -0
  57. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_dropduplicates.py +0 -0
  58. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_dropna.py +0 -0
  59. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_filter.py +0 -0
  60. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_groupby.py +0 -0
  61. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_join.py +0 -0
  62. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_orderby.py +0 -0
  63. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_pivot.py +0 -0
  64. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_select.py +0 -0
  65. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_withcolumn.py +0 -0
  66. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/polars/.gitkeep +0 -0
  67. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/__init__.py +0 -0
  68. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/__init__.py +0 -0
  69. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/aggregate.py +0 -0
  70. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/cast.py +0 -0
  71. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/distinct.py +0 -0
  72. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/drop.py +0 -0
  73. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/dropduplicates.py +0 -0
  74. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/dropna.py +0 -0
  75. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/groupby.py +0 -0
  76. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/orderby.py +0 -0
  77. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/pivot.py +0 -0
  78. {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/withcolumn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Samara
3
- Version: 0.2
3
+ Version: 0.4
4
4
  Summary: Config Driven ETL Framework
5
5
  License-File: LICENSE
6
6
  Author: Krijn van der Burg
@@ -9,7 +9,6 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.13
10
10
  Classifier: Programming Language :: Python :: 3.14
11
11
  Requires-Dist: click (>=8.1.0,<9.0.0)
12
- Requires-Dist: email-validator (>=2.2.0,<3.0.0)
13
12
  Requires-Dist: numpy (<2.0.0)
14
13
  Requires-Dist: opentelemetry-api (>=1.37.0,<2.0.0)
15
14
  Requires-Dist: opentelemetry-exporter-otlp (>=1.37.0,<2.0.0)
@@ -22,6 +21,7 @@ Requires-Dist: pyjson5 (>=1.6.9,<2.0.0)
22
21
  Requires-Dist: pyspark (>=4.0.1,<5.0.0)
23
22
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
24
23
  Requires-Dist: requests (>=2.32.5,<3.0.0)
24
+ Requires-Dist: rich (>=14.3.3,<15.0.0)
25
25
  Requires-Dist: structlog (>=25.4.0,<26.0.0)
26
26
  Description-Content-Type: text/markdown
27
27
 
@@ -73,7 +73,6 @@ poetry install
73
73
  ### Run an example pipeline
74
74
  ```bash
75
75
  python -m samara run \
76
- --alert-filepath="examples/yaml_products_cleanup/alert.yaml" \
77
76
  --workflow-filepath="examples/yaml_products_cleanup/job.yaml"
78
77
  ```
79
78
 
@@ -85,7 +84,6 @@ Samara's documentation guides you through installation, configuration, and devel
85
84
  - **[CLI Reference](./docs/cli.md)** - Command-line interface options and examples
86
85
  - **[Configuration Reference](./docs/README.md)** - Complete syntax guide for all configuration options
87
86
  - **[Workflow System](./docs/workflow/README.md)** - ETL pipeline configuration (extracts, transforms, loads)
88
- - **[Alert System](./docs/alert/README.md)** - Error handling and notification configuration
89
87
  - **[Architecture](./docs/architecture.md)** - Design principles and framework structure
90
88
  - **[Custom Extensions](./docs/architecture.md#extending-with-custom-transforms)** - Building your own transforms
91
89
 
@@ -46,7 +46,6 @@ poetry install
46
46
  ### Run an example pipeline
47
47
  ```bash
48
48
  python -m samara run \
49
- --alert-filepath="examples/yaml_products_cleanup/alert.yaml" \
50
49
  --workflow-filepath="examples/yaml_products_cleanup/job.yaml"
51
50
  ```
52
51
 
@@ -58,7 +57,6 @@ Samara's documentation guides you through installation, configuration, and devel
58
57
  - **[CLI Reference](./docs/cli.md)** - Command-line interface options and examples
59
58
  - **[Configuration Reference](./docs/README.md)** - Complete syntax guide for all configuration options
60
59
  - **[Workflow System](./docs/workflow/README.md)** - ETL pipeline configuration (extracts, transforms, loads)
61
- - **[Alert System](./docs/alert/README.md)** - Error handling and notification configuration
62
60
  - **[Architecture](./docs/architecture.md)** - Design principles and framework structure
63
61
  - **[Custom Extensions](./docs/architecture.md#extending-with-custom-transforms)** - Building your own transforms
64
62
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Samara"
3
- version = "0.2"
3
+ version = "0.4"
4
4
  description = "Config Driven ETL Framework"
5
5
  authors = ["Krijn van der Burg"]
6
6
  readme = "README.md"
@@ -17,13 +17,13 @@ requests = "^2.32.5"
17
17
  structlog = "^25.4.0"
18
18
  pydantic = "^2.11.9"
19
19
  pydantic-settings = "^2.8.0"
20
- email-validator = "^2.2.0"
21
20
  pyjson5 = "^1.6.9"
22
21
  click = "^8.1.0"
23
22
  # OpenTelemetry core packages - using compatible versions
24
23
  opentelemetry-api = "^1.37.0"
25
24
  opentelemetry-sdk = "^1.37.0"
26
25
  opentelemetry-exporter-otlp = "^1.37.0"
26
+ rich = "^14.3.3"
27
27
 
28
28
  [tool.poetry.group.test.dependencies]
29
29
  pre_commit = "*"
@@ -7,8 +7,6 @@ engines and extensible components.
7
7
 
8
8
  Key capabilities:
9
9
  - Define pipelines via configuration (sources, transforms, destinations)
10
- - Multi-engine architecture (Pandas, Polars, and more)
11
- - Configurable alert system with multiple notification channels
12
10
  - Event-triggered custom actions at pipeline stages
13
11
  - Engine-agnostic configuration supporting different backends
14
12
 
@@ -22,7 +20,6 @@ Example:
22
20
 
23
21
  See Also:
24
22
  - Configuration format documentation in docs/
25
- - Alert system setup in docs/alert/
26
23
  - Available transforms and operations documentation
27
24
  """
28
25
 
@@ -39,6 +36,7 @@ from abc import ABC
39
36
  from datetime import datetime, timezone
40
37
 
41
38
  from pydantic import BaseModel as PydanticBaseModel
39
+ from pydantic import ConfigDict
42
40
 
43
41
  # Generate a run identifier as early as possible so the entire application
44
42
  # can reference the same run id. This is created at import time and is
@@ -99,3 +97,6 @@ class BaseModel(PydanticBaseModel, ABC):
99
97
  See Also:
100
98
  pydantic.BaseModel: For configuration validation framework details
101
99
  """
100
+
101
+ # Consider enabling extra="forbid"
102
+ model_config = ConfigDict()
@@ -2,24 +2,20 @@
2
2
 
3
3
  This module provides command-line interface commands for managing ETL pipelines
4
4
  through configuration files. It focuses on three core operations: validating
5
- pipeline configurations with optional alert testing, executing pipelines with
6
- integrated alerting, and exporting JSON schemas for configuration documentation.
5
+ pipeline configurations, executing pipelines, and exporting JSON schemas for
6
+ configuration documentation.
7
7
 
8
8
  All commands support detailed error handling and proper exit codes to facilitate
9
9
  CI/CD integration and operational monitoring.
10
10
  """
11
11
 
12
12
  import json
13
- import os
14
13
  from pathlib import Path
15
14
 
16
15
  import click
17
16
 
18
- from samara.alert import AlertController
19
17
  from samara.exceptions import (
20
18
  ExitCode,
21
- SamaraAlertConfigurationError,
22
- SamaraAlertTestError,
23
19
  SamaraIOError,
24
20
  SamaraValidationError,
25
21
  SamaraWorkflowConfigurationError,
@@ -76,7 +72,7 @@ def cli(
76
72
 
77
73
  Build and execute data workflows through declarative JSON/YAML configuration
78
74
  instead of writing code. Define extracts, transforms, and loads with built-in
79
- support for alerts, validation, and schema management.
75
+ support for validation and schema management.
80
76
 
81
77
  Args:
82
78
  log_level: The logging level as a string. Must be one of DEBUG, INFO,
@@ -94,7 +90,7 @@ def cli(
94
90
 
95
91
  Commands:
96
92
  validate: Validate workflow configurations without execution
97
- run: Execute workflow with integrated alerting
93
+ run: Execute workflow
98
94
  export-schema: Generate JSON schema for workflow configs
99
95
  """
100
96
  settings = get_settings()
@@ -112,120 +108,46 @@ def cli(
112
108
 
113
109
 
114
110
  @cli.command()
115
- @click.option(
116
- "--alert-filepath",
117
- required=True,
118
- type=click.Path(exists=False, path_type=Path),
119
- help="Path to alert configuration file",
120
- )
121
111
  @click.option(
122
112
  "--workflow-filepath",
123
113
  required=True,
124
114
  type=click.Path(exists=False, path_type=Path),
125
115
  help="Path to workflow configuration file",
126
116
  )
127
- @click.option(
128
- "--test-exception",
129
- type=str,
130
- default=None,
131
- help="Test exception message to trigger alert testing",
132
- )
133
- @click.option(
134
- "--test-env-var",
135
- multiple=True,
136
- type=str,
137
- help="Test env vars (KEY=VALUE)",
138
- )
139
117
  @trace_span("validate_workflow")
140
118
  def validate(
141
- alert_filepath: Path,
142
119
  workflow_filepath: Path,
143
- test_exception: str | None,
144
- test_env_var: tuple[str, ...],
145
120
  ) -> None:
146
- """Validate workflow configuration files with optional alert testing.
121
+ """Validate workflow configuration files.
147
122
 
148
- Load and validate both alert and workflow configuration files to ensure they
149
- conform to expected schemas and contain valid settings. This command performs
150
- fail-fast validation without alerting on configuration errors (unlike the run
151
- command), making it suitable for local development and CI/CD workflows where
152
- validation failures should not trigger alerts.
153
-
154
- Optionally trigger a test alert to verify alert system functionality using
155
- a test exception message or environment variables.
123
+ Load and validate the workflow configuration file to ensure it conforms
124
+ to the expected schema and contains valid settings. This command performs
125
+ fail-fast validation, making it suitable for local development and CI/CD
126
+ workflows.
156
127
 
157
128
  Args:
158
- alert_filepath: Path to the alert configuration file in JSON or YAML
159
- format. The file must exist and contain valid alert configuration
160
- with triggers and channels.
161
129
  workflow_filepath: Path to the workflow configuration file in JSON
162
130
  or YAML format. The file must exist and define valid workflow
163
131
  extracts, transforms, and loads.
164
- test_exception: Optional test exception message string. When provided,
165
- triggers a test alert to verify alert system functionality. If
166
- provided with test_env_var, this takes precedence for the message.
167
- test_env_var: Optional environment variables to set before validation
168
- in KEY=VALUE format. Useful for testing environment-dependent
169
- configurations without affecting system environment permanently.
170
132
 
171
133
  Raises:
172
134
  click.exceptions.Exit: Exits with appropriate exit code on error.
173
-
174
- Note:
175
- This command does NOT send alerts on configuration errors, only on
176
- test alerts if explicitly requested. This prevents alert fatigue
177
- during development and validation cycles. For the actual workflow
178
- execution with alert integration, use the 'run' command.
179
135
  """
180
136
  try:
181
137
  logger.info("Starting `validate` command")
182
138
  logger.info("Workflow config: %s", str(workflow_filepath))
183
- logger.info("Alert config: %s", str(alert_filepath))
184
-
185
- # Parse test env vars
186
- test_env_vars = None
187
- if test_env_var:
188
- test_env_vars = {}
189
- for env_var_str in test_env_var:
190
- key, value = env_var_str.split("=", 1)
191
- test_env_vars[key] = value
192
-
193
- # Set test env vars if provided
194
- if test_env_vars:
195
- for key, value in test_env_vars.items():
196
- os.environ[key] = value
197
-
198
- try:
199
- alert = AlertController.from_file(filepath=alert_filepath)
200
- except SamaraIOError as e:
201
- logger.error("Cannot access alert configuration file: %s", e)
202
- raise click.exceptions.Exit(e.exit_code)
203
- except SamaraAlertConfigurationError as e:
204
- logger.error("Alert configuration is invalid: %s", e)
205
- raise click.exceptions.Exit(e.exit_code)
206
139
 
207
140
  try:
208
141
  _ = WorkflowController.from_file(filepath=workflow_filepath)
209
- # Not alerting on exceptions as a validate command is often run locally or from CICD
210
- # and thus an alert would be drowning out real alerts
211
142
  except SamaraIOError as e:
212
143
  logger.error("Cannot access workflow configuration file: %s", e)
213
- raise click.exceptions.Exit(e.exit_code)
144
+ raise click.exceptions.Exit(e.exit_code) from e
214
145
  except SamaraWorkflowConfigurationError as e:
215
146
  logger.error("Workflow configuration is invalid: %s", e)
216
- raise click.exceptions.Exit(e.exit_code)
147
+ raise click.exceptions.Exit(e.exit_code) from e
217
148
  except SamaraValidationError as e:
218
149
  logger.error("Validation failed: %s", e)
219
- raise click.exceptions.Exit(e.exit_code)
220
-
221
- # Trigger test exception if specified (either message or env vars)
222
- if test_exception or test_env_vars:
223
- try:
224
- message = test_exception or "Test alert triggered"
225
- raise SamaraAlertTestError(message)
226
- except SamaraAlertTestError as e:
227
- alert.evaluate_trigger_and_alert(title="Test Alert", body="Test alert", exception=e)
228
- raise click.exceptions.Exit(e.exit_code)
150
+ raise click.exceptions.Exit(e.exit_code) from e
229
151
 
230
152
  logger.info("Workflow validation completed successfully")
231
153
  logger.info("Command executed successfully with exit code %d (%s).", ExitCode.SUCCESS, ExitCode.SUCCESS.name)
@@ -243,12 +165,6 @@ def validate(
243
165
 
244
166
 
245
167
  @cli.command()
246
- @click.option(
247
- "--alert-filepath",
248
- required=True,
249
- type=click.Path(exists=False, path_type=Path),
250
- help="Path to alert configuration file",
251
- )
252
168
  @click.option(
253
169
  "--workflow-filepath",
254
170
  required=True,
@@ -257,47 +173,25 @@ def validate(
257
173
  )
258
174
  @trace_span("run_pipeline")
259
175
  def run(
260
- alert_filepath: Path,
261
176
  workflow_filepath: Path,
262
177
  ) -> None:
263
- """Execute the workflow with integrated alert monitoring.
178
+ """Execute the workflow.
264
179
 
265
- Load workflow and alert configurations, then execute the complete workflow.
180
+ Load the workflow configuration, then execute the complete workflow.
266
181
  The workflow processes all defined jobs in sequence, applying configured
267
182
  transforms to ingest, transform, and load data according to specifications.
268
- Errors during workflow execution are captured and alerts are sent based on
269
- configured alert rules and triggers.
270
183
 
271
184
  Args:
272
- alert_filepath: Path to the alert configuration file in JSON or YAML
273
- format. Defines alert channels (email, HTTP, file) and trigger rules
274
- that determine when and how alerts are sent during execution.
275
185
  workflow_filepath: Path to the workflow configuration file in JSON or YAML
276
186
  format. Defines the complete workflow including data sources,
277
187
  transformation chains, and output destinations.
278
188
 
279
189
  Raises:
280
190
  click.exceptions.Exit: Exits with appropriate exit code on error.
281
-
282
- Note:
283
- All exceptions during workflow execution trigger alert evaluation,
284
- allowing configured alert rules to send notifications based on
285
- error type and severity. This enables operational visibility into
286
- workflow failures and automating incident response workflows.
287
191
  """
288
192
  try:
289
193
  logger.info("Starting `run` command")
290
194
  logger.info("Workflow config: %s", str(workflow_filepath))
291
- logger.info("Alert config: %s", str(alert_filepath))
292
-
293
- try:
294
- alert = AlertController.from_file(filepath=alert_filepath)
295
- except SamaraIOError as e:
296
- logger.error("Cannot access alert configuration file: %s", e)
297
- raise click.exceptions.Exit(e.exit_code)
298
- except SamaraAlertConfigurationError as e:
299
- logger.error("Alert configuration is invalid: %s", e)
300
- raise click.exceptions.Exit(e.exit_code)
301
195
 
302
196
  try:
303
197
  workflow = WorkflowController.from_file(filepath=workflow_filepath)
@@ -309,30 +203,16 @@ def run(
309
203
  )
310
204
  except SamaraIOError as e:
311
205
  logger.error("Cannot access workflow configuration file: %s", e)
312
- alert.evaluate_trigger_and_alert(
313
- title="Workflow Configuration File Error",
314
- body="Failed to read workflow configuration file",
315
- exception=e,
316
- )
317
- raise click.exceptions.Exit(e.exit_code)
206
+ raise click.exceptions.Exit(e.exit_code) from e
318
207
  except SamaraWorkflowConfigurationError as e:
319
208
  logger.error("Workflow configuration is invalid: %s", e)
320
- alert.evaluate_trigger_and_alert(
321
- title="Workflow Configuration Error", body="Invalid workflow configuration", exception=e
322
- )
323
- raise click.exceptions.Exit(e.exit_code)
209
+ raise click.exceptions.Exit(e.exit_code) from e
324
210
  except SamaraValidationError as e:
325
211
  logger.error("Configuration validation failed: %s", e)
326
- alert.evaluate_trigger_and_alert(
327
- title="Workflow Validation Error", body="Configuration validation failed", exception=e
328
- )
329
- raise click.exceptions.Exit(e.exit_code)
212
+ raise click.exceptions.Exit(e.exit_code) from e
330
213
  except SamaraWorkflowError as e:
331
214
  logger.error("Workflow job failed: %s", e)
332
- alert.evaluate_trigger_and_alert(
333
- title="Workflow Execution Error", body="Workflow error during execution", exception=e
334
- )
335
- raise click.exceptions.Exit(e.exit_code)
215
+ raise click.exceptions.Exit(e.exit_code) from e
336
216
 
337
217
  except click.exceptions.Exit:
338
218
  # Re-raise Click's Exit exceptions (these are our controlled exits with proper codes)
@@ -14,9 +14,6 @@ meaningful error states to the operating system.
14
14
  """
15
15
 
16
16
  import enum
17
- from typing import TypeVar
18
-
19
- K = TypeVar("K") # Key type
20
17
 
21
18
 
22
19
  class ExitCode(enum.IntEnum):
@@ -31,11 +28,10 @@ class ExitCode(enum.IntEnum):
31
28
  INVALID_ARGUMENTS = 10
32
29
  IO_ERROR = 20
33
30
  CONFIGURATION_ERROR = 30
34
- ALERT_CONFIGURATION_ERROR = 31
35
31
  WORKFLOW_CONFIGURATION_ERROR = 32
36
32
  VALIDATION_ERROR = 40
37
- ALERT_TEST_ERROR = 41
38
33
  JOB_ERROR = 50
34
+ ACTION_ERROR = 51
39
35
  KEYBOARD_INTERRUPT = 98
40
36
  UNEXPECTED_ERROR = 99
41
37
 
@@ -82,22 +78,6 @@ class SamaraIOError(SamaraError):
82
78
  super().__init__(message=message, exit_code=ExitCode.IO_ERROR)
83
79
 
84
80
 
85
- class SamaraAlertConfigurationError(SamaraError):
86
- """Raise when alert configuration is invalid.
87
-
88
- Indicates issues with alert definition JSON/YAML including invalid
89
- channels, triggers, or template configuration.
90
- """
91
-
92
- def __init__(self, message: str) -> None:
93
- """Initialize the exception.
94
-
95
- Args:
96
- message: Description of the configuration error
97
- """
98
- super().__init__(message=message, exit_code=ExitCode.CONFIGURATION_ERROR)
99
-
100
-
101
81
  class SamaraWorkflowConfigurationError(SamaraError):
102
82
  """Raise when workflow configuration is invalid.
103
83
 
@@ -130,33 +110,34 @@ class SamaraValidationError(SamaraError):
130
110
  super().__init__(message=message, exit_code=ExitCode.VALIDATION_ERROR)
131
111
 
132
112
 
133
- class SamaraAlertTestError(SamaraError):
134
- """Raise when alert system testing fails.
113
+ class SamaraWorkflowError(SamaraError):
114
+ """Raise when ETL job execution fails.
135
115
 
136
- Indicates failure during alert channel validation or test execution,
137
- including delivery failures or notification errors.
116
+ Covers errors during data extraction, transformation, or loading phases
117
+ including engine failures or transformation logic errors.
138
118
  """
139
119
 
140
120
  def __init__(self, message: str) -> None:
141
121
  """Initialize the exception.
142
122
 
143
123
  Args:
144
- message: Description of the alert test error
124
+ message: Description of the job execution error
145
125
  """
146
- super().__init__(message=message, exit_code=ExitCode.ALERT_TEST_ERROR)
126
+ super().__init__(message=message, exit_code=ExitCode.JOB_ERROR)
147
127
 
148
128
 
149
- class SamaraWorkflowError(SamaraError):
150
- """Raise when ETL job execution fails.
129
+ class SamaraActionError(SamaraError):
130
+ """Raise when a lifecycle hook action fails to execute.
151
131
 
152
- Covers errors during data extraction, transformation, or loading phases
153
- including engine failures or transformation logic errors.
132
+ Wraps any exception raised by an action's implementation (e.g. HTTP,
133
+ email, file actions) so callers can catch a single, specific type
134
+ instead of a bare `Exception`.
154
135
  """
155
136
 
156
137
  def __init__(self, message: str) -> None:
157
138
  """Initialize the exception.
158
139
 
159
140
  Args:
160
- message: Description of the job execution error
141
+ message: Description of the action execution error
161
142
  """
162
- super().__init__(message=message, exit_code=ExitCode.JOB_ERROR)
143
+ super().__init__(message=message, exit_code=ExitCode.ACTION_ERROR)
@@ -27,8 +27,9 @@ Typical Usage:
27
27
  """
28
28
 
29
29
  from functools import lru_cache
30
+ from typing import Literal
30
31
 
31
- from pydantic import Field
32
+ from pydantic import AnyHttpUrl, Field, field_validator
32
33
  from pydantic_settings import BaseSettings, SettingsConfigDict
33
34
 
34
35
 
@@ -142,13 +143,31 @@ class AppSettings(BaseSettings):
142
143
  case_sensitive=False,
143
144
  )
144
145
 
145
- log_level: str | None = Field(default=None, description="Logging level of the system")
146
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = Field(
147
+ default=None, description="Logging level of the system"
148
+ )
146
149
  environment: str | None = Field(default=None, description="Deployment environment (dev, test, acc, prod)")
147
150
  trace_parent: str | None = Field(default=None, description="W3C Trace Context traceparent for distributed tracing")
148
151
  trace_state: str | None = Field(default=None, description="W3C Trace Context tracestate for distributed tracing")
149
152
  otlp_traces_endpoint: str | None = Field(default=None, description="OTLP endpoint for exporting traces")
150
153
  otlp_logs_endpoint: str | None = Field(default=None, description="OTLP endpoint for exporting logs")
151
154
 
155
+ @field_validator("log_level", mode="before")
156
+ @classmethod
157
+ def _normalize_log_level(cls, value: str | None) -> str | None:
158
+ """Uppercase the log level so env values like 'debug' validate."""
159
+ if isinstance(value, str):
160
+ return value.upper()
161
+ return value
162
+
163
+ @field_validator("otlp_traces_endpoint", "otlp_logs_endpoint")
164
+ @classmethod
165
+ def _validate_url(cls, value: str | None) -> str | None:
166
+ """Reject OTLP endpoints that are not valid HTTP(S) URLs."""
167
+ if value is not None:
168
+ AnyHttpUrl(value) # raises pydantic.ValidationError on invalid URL
169
+ return value
170
+
152
171
 
153
172
  @lru_cache
154
173
  def get_settings() -> AppSettings: