Samara 0.3__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.3 → samara-0.4}/PKG-INFO +1 -4
  2. {samara-0.3 → samara-0.4}/README.md +0 -2
  3. {samara-0.3 → samara-0.4}/pyproject.toml +1 -2
  4. {samara-0.3 → samara-0.4}/src/samara/__init__.py +4 -2
  5. {samara-0.3 → samara-0.4}/src/samara/cli.py +18 -138
  6. {samara-0.3 → samara-0.4}/src/samara/exceptions.py +14 -30
  7. {samara-0.3 → samara-0.4}/src/samara/settings.py +21 -2
  8. {samara-0.3 → samara-0.4}/src/samara/utils/http.py +13 -13
  9. {samara-0.3 → samara-0.4}/src/samara/workflow/actions/base.py +7 -4
  10. {samara-0.3 → samara-0.4}/src/samara/workflow/actions/http.py +9 -4
  11. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/hooks.py +28 -8
  12. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/model_load.py +1 -1
  13. samara-0.3/src/samara/alert/__init__.py +0 -16
  14. samara-0.3/src/samara/alert/channels/__init__.py +0 -30
  15. samara-0.3/src/samara/alert/channels/base.py +0 -124
  16. samara-0.3/src/samara/alert/channels/email.py +0 -135
  17. samara-0.3/src/samara/alert/channels/file.py +0 -101
  18. samara-0.3/src/samara/alert/channels/http.py +0 -146
  19. samara-0.3/src/samara/alert/controller.py +0 -219
  20. samara-0.3/src/samara/alert/rules/__init__.py +0 -28
  21. samara-0.3/src/samara/alert/rules/base.py +0 -86
  22. samara-0.3/src/samara/alert/rules/env_vars_matches.py +0 -120
  23. samara-0.3/src/samara/alert/rules/exception_regex.py +0 -94
  24. samara-0.3/src/samara/alert/template.py +0 -103
  25. samara-0.3/src/samara/alert/trigger.py +0 -155
  26. {samara-0.3 → samara-0.4}/LICENSE +0 -0
  27. {samara-0.3 → samara-0.4}/src/samara/__main__.py +0 -0
  28. {samara-0.3 → samara-0.4}/src/samara/telemetry.py +0 -0
  29. {samara-0.3 → samara-0.4}/src/samara/types.py +0 -0
  30. {samara-0.3 → samara-0.4}/src/samara/utils/__init__.py +0 -0
  31. {samara-0.3 → samara-0.4}/src/samara/utils/file.py +0 -0
  32. {samara-0.3 → samara-0.4}/src/samara/utils/logger.py +0 -0
  33. {samara-0.3 → samara-0.4}/src/samara/workflow/__init__.py +0 -0
  34. {samara-0.3 → samara-0.4}/src/samara/workflow/actions/__init__.py +0 -0
  35. {samara-0.3 → samara-0.4}/src/samara/workflow/actions/move_or_copy_job_files.py +0 -0
  36. {samara-0.3 → samara-0.4}/src/samara/workflow/controller.py +0 -0
  37. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/__init__.py +0 -0
  38. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/__init__.py +0 -0
  39. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/model_extract.py +0 -0
  40. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/model_job.py +0 -0
  41. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/model_transform.py +0 -0
  42. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/__init__.py +0 -0
  43. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_aggregate.py +0 -0
  44. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_cast.py +0 -0
  45. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_distinct.py +0 -0
  46. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_drop.py +0 -0
  47. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_dropduplicates.py +0 -0
  48. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_dropna.py +0 -0
  49. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_filter.py +0 -0
  50. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_groupby.py +0 -0
  51. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_join.py +0 -0
  52. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_orderby.py +0 -0
  53. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_pivot.py +0 -0
  54. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_select.py +0 -0
  55. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_withcolumn.py +0 -0
  56. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/polars/.gitkeep +0 -0
  57. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/__init__.py +0 -0
  58. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/extract.py +0 -0
  59. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/job.py +0 -0
  60. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/load.py +0 -0
  61. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/schema.py +0 -0
  62. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/session.py +0 -0
  63. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transform.py +0 -0
  64. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/__init__.py +0 -0
  65. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/aggregate.py +0 -0
  66. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/base.py +0 -0
  67. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/cast.py +0 -0
  68. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/distinct.py +0 -0
  69. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/drop.py +0 -0
  70. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/dropduplicates.py +0 -0
  71. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/dropna.py +0 -0
  72. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/filter.py +0 -0
  73. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/groupby.py +0 -0
  74. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/join.py +0 -0
  75. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/orderby.py +0 -0
  76. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/pivot.py +0 -0
  77. {samara-0.3 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/select.py +0 -0
  78. {samara-0.3 → 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.3
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)
@@ -74,7 +73,6 @@ poetry install
74
73
  ### Run an example pipeline
75
74
  ```bash
76
75
  python -m samara run \
77
- --alert-filepath="examples/yaml_products_cleanup/alert.yaml" \
78
76
  --workflow-filepath="examples/yaml_products_cleanup/job.yaml"
79
77
  ```
80
78
 
@@ -86,7 +84,6 @@ Samara's documentation guides you through installation, configuration, and devel
86
84
  - **[CLI Reference](./docs/cli.md)** - Command-line interface options and examples
87
85
  - **[Configuration Reference](./docs/README.md)** - Complete syntax guide for all configuration options
88
86
  - **[Workflow System](./docs/workflow/README.md)** - ETL pipeline configuration (extracts, transforms, loads)
89
- - **[Alert System](./docs/alert/README.md)** - Error handling and notification configuration
90
87
  - **[Architecture](./docs/architecture.md)** - Design principles and framework structure
91
88
  - **[Custom Extensions](./docs/architecture.md#extending-with-custom-transforms)** - Building your own transforms
92
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.3"
3
+ version = "0.4"
4
4
  description = "Config Driven ETL Framework"
5
5
  authors = ["Krijn van der Burg"]
6
6
  readme = "README.md"
@@ -17,7 +17,6 @@ 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
@@ -7,7 +7,6 @@ engines and extensible components.
7
7
 
8
8
  Key capabilities:
9
9
  - Define pipelines via configuration (sources, transforms, destinations)
10
- - Configurable alert system with multiple notification channels
11
10
  - Event-triggered custom actions at pipeline stages
12
11
  - Engine-agnostic configuration supporting different backends
13
12
 
@@ -21,7 +20,6 @@ Example:
21
20
 
22
21
  See Also:
23
22
  - Configuration format documentation in docs/
24
- - Alert system setup in docs/alert/
25
23
  - Available transforms and operations documentation
26
24
  """
27
25
 
@@ -38,6 +36,7 @@ from abc import ABC
38
36
  from datetime import datetime, timezone
39
37
 
40
38
  from pydantic import BaseModel as PydanticBaseModel
39
+ from pydantic import ConfigDict
41
40
 
42
41
  # Generate a run identifier as early as possible so the entire application
43
42
  # can reference the same run id. This is created at import time and is
@@ -98,3 +97,6 @@ class BaseModel(PydanticBaseModel, ABC):
98
97
  See Also:
99
98
  pydantic.BaseModel: For configuration validation framework details
100
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)
@@ -28,11 +28,10 @@ class ExitCode(enum.IntEnum):
28
28
  INVALID_ARGUMENTS = 10
29
29
  IO_ERROR = 20
30
30
  CONFIGURATION_ERROR = 30
31
- ALERT_CONFIGURATION_ERROR = 31
32
31
  WORKFLOW_CONFIGURATION_ERROR = 32
33
32
  VALIDATION_ERROR = 40
34
- ALERT_TEST_ERROR = 41
35
33
  JOB_ERROR = 50
34
+ ACTION_ERROR = 51
36
35
  KEYBOARD_INTERRUPT = 98
37
36
  UNEXPECTED_ERROR = 99
38
37
 
@@ -79,22 +78,6 @@ class SamaraIOError(SamaraError):
79
78
  super().__init__(message=message, exit_code=ExitCode.IO_ERROR)
80
79
 
81
80
 
82
- class SamaraAlertConfigurationError(SamaraError):
83
- """Raise when alert configuration is invalid.
84
-
85
- Indicates issues with alert definition JSON/YAML including invalid
86
- channels, triggers, or template configuration.
87
- """
88
-
89
- def __init__(self, message: str) -> None:
90
- """Initialize the exception.
91
-
92
- Args:
93
- message: Description of the configuration error
94
- """
95
- super().__init__(message=message, exit_code=ExitCode.CONFIGURATION_ERROR)
96
-
97
-
98
81
  class SamaraWorkflowConfigurationError(SamaraError):
99
82
  """Raise when workflow configuration is invalid.
100
83
 
@@ -127,33 +110,34 @@ class SamaraValidationError(SamaraError):
127
110
  super().__init__(message=message, exit_code=ExitCode.VALIDATION_ERROR)
128
111
 
129
112
 
130
- class SamaraAlertTestError(SamaraError):
131
- """Raise when alert system testing fails.
113
+ class SamaraWorkflowError(SamaraError):
114
+ """Raise when ETL job execution fails.
132
115
 
133
- Indicates failure during alert channel validation or test execution,
134
- including delivery failures or notification errors.
116
+ Covers errors during data extraction, transformation, or loading phases
117
+ including engine failures or transformation logic errors.
135
118
  """
136
119
 
137
120
  def __init__(self, message: str) -> None:
138
121
  """Initialize the exception.
139
122
 
140
123
  Args:
141
- message: Description of the alert test error
124
+ message: Description of the job execution error
142
125
  """
143
- super().__init__(message=message, exit_code=ExitCode.ALERT_TEST_ERROR)
126
+ super().__init__(message=message, exit_code=ExitCode.JOB_ERROR)
144
127
 
145
128
 
146
- class SamaraWorkflowError(SamaraError):
147
- """Raise when ETL job execution fails.
129
+ class SamaraActionError(SamaraError):
130
+ """Raise when a lifecycle hook action fails to execute.
148
131
 
149
- Covers errors during data extraction, transformation, or loading phases
150
- 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`.
151
135
  """
152
136
 
153
137
  def __init__(self, message: str) -> None:
154
138
  """Initialize the exception.
155
139
 
156
140
  Args:
157
- message: Description of the job execution error
141
+ message: Description of the action execution error
158
142
  """
159
- 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:
@@ -1,10 +1,9 @@
1
1
  """HTTP utilities for Samara's configuration-driven data pipeline framework.
2
2
 
3
- Provides reusable HTTP functionality for alert channels and event hook actions,
4
- enabling reliable communication through configurable retry logic, timeout handling,
5
- and standardized request execution. Supports the declarative configuration model
6
- by allowing HTTP endpoints and behaviors to be specified in configuration files
7
- rather than code.
3
+ Provides reusable HTTP functionality for event hook actions, enabling reliable
4
+ communication through configurable retry logic, timeout handling, and standardized
5
+ request execution. Supports the declarative configuration model by allowing HTTP
6
+ endpoints and behaviors to be specified in configuration files rather than code.
8
7
  """
9
8
 
10
9
  import json
@@ -53,13 +52,13 @@ class Retry(BaseModel):
53
52
 
54
53
 
55
54
  class HttpBase(BaseModel):
56
- """Base HTTP configuration for alerts and event hooks in Samara pipelines.
55
+ """Base HTTP configuration for event hooks in Samara pipelines.
57
56
 
58
- Provides shared HTTP request handling for alert notifications and event-triggered
59
- actions. Supports the framework's configuration-driven model by allowing HTTP
60
- endpoints, methods, headers, and retry behavior to be defined in configuration
61
- files. Used by alert channels and actions to communicate with external HTTP
62
- endpoints, webhooks, and services.
57
+ Provides shared HTTP request handling for event-triggered actions. Supports
58
+ the framework's configuration-driven model by allowing HTTP endpoints,
59
+ methods, headers, and retry behavior to be defined in configuration files.
60
+ Used by actions to communicate with external HTTP endpoints, webhooks, and
61
+ services.
63
62
 
64
63
  Attributes:
65
64
  url: HTTP endpoint URL for sending requests.
@@ -113,8 +112,8 @@ class HttpBase(BaseModel):
113
112
  """Execute an HTTP request with configurable retry logic and error handling.
114
113
 
115
114
  Sends an HTTP request with the configured method, headers, timeout, and retry
116
- behavior. Handles transient failures gracefully through exponential backoff
117
- retry logic. On success, logs the request completion; on final failure after
115
+ behavior. Handles transient failures gracefully through fixed-delay retry
116
+ logic. On success, logs the request completion; on final failure after
118
117
  all retries, logs the error and raises the underlying exception.
119
118
 
120
119
  Args:
@@ -151,3 +150,4 @@ class HttpBase(BaseModel):
151
150
  time.sleep(self.retry.delay_in_seconds)
152
151
  else:
153
152
  logger.error("HTTP request failed after %d attempts: %s", self.retry.max_attempts + 1, e)
153
+ raise
@@ -81,6 +81,10 @@ class ActionBase(BaseModel):
81
81
  ... recipients=["user@example.com"]
82
82
  ... )
83
83
  >>> action.execute() # Executes the email action
84
+
85
+ Raises:
86
+ SamaraActionError: If the action's implementation fails. Implementations
87
+ must catch their own specific exception types and raise this.
84
88
  """
85
89
  if not self.enabled:
86
90
  logger.debug("Action '%s' is disabled; skipping execution.", self.id_)
@@ -96,11 +100,10 @@ class ActionBase(BaseModel):
96
100
  writing to files).
97
101
 
98
102
  Raises:
99
- NotImplementedError: When called on ActionBase directly or if subclass
100
- does not provide an implementation.
103
+ SamaraActionError: Implementations must catch their own known failure
104
+ modes (e.g. `requests.RequestException`, `OSError`) and raise this
105
+ so callers can rely on a single, specific exception type.
101
106
 
102
107
  Note:
103
108
  This method is only called by execute() if the action is enabled.
104
- Implementation should handle any failures appropriately, using logging
105
- for debugging and raising exceptions for unexpected errors.
106
109
  """
@@ -8,9 +8,11 @@ payloads, timeouts, and retry logic for robust external integrations.
8
8
 
9
9
  from typing import Any, Literal
10
10
 
11
+ import requests
11
12
  from pydantic import Field
12
13
  from typing_extensions import override
13
14
 
15
+ from samara.exceptions import SamaraActionError
14
16
  from samara.telemetry import trace_span
15
17
  from samara.utils.http import HttpBase
16
18
  from samara.utils.logger import get_logger
@@ -118,9 +120,9 @@ class HttpAction(HttpBase, ActionBase):
118
120
  successful requests and failures for observability.
119
121
 
120
122
  Raises:
121
- requests.RequestException: If the HTTP request fails after exhausting
122
- all configured retry attempts, including connection errors,
123
- timeouts, or non-2xx HTTP responses.
123
+ SamaraActionError: If the HTTP request fails after exhausting all
124
+ configured retry attempts, including connection errors, timeouts,
125
+ or non-2xx HTTP responses.
124
126
 
125
127
  Note:
126
128
  Even if this action fails, the pipeline continues execution as
@@ -128,5 +130,8 @@ class HttpAction(HttpBase, ActionBase):
128
130
  details if requests fail unexpectedly.
129
131
  """
130
132
  logger.info("Executing HTTP action: %s", self.id_)
131
- self._make_http_request(self.payload)
133
+ try:
134
+ self._make_http_request(self.payload)
135
+ except requests.RequestException as e:
136
+ raise SamaraActionError(f"Action '{self.id_}' failed: {e}") from e
132
137
  logger.info("HTTP action completed: %s", self.id_)