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.
- {samara-0.2 → samara-0.4}/PKG-INFO +2 -4
- {samara-0.2 → samara-0.4}/README.md +0 -2
- {samara-0.2 → samara-0.4}/pyproject.toml +2 -2
- {samara-0.2 → samara-0.4}/src/samara/__init__.py +4 -3
- {samara-0.2 → samara-0.4}/src/samara/cli.py +18 -138
- {samara-0.2 → samara-0.4}/src/samara/exceptions.py +14 -33
- {samara-0.2 → samara-0.4}/src/samara/settings.py +21 -2
- {samara-0.2 → samara-0.4}/src/samara/utils/file.py +2 -36
- {samara-0.2 → samara-0.4}/src/samara/utils/http.py +13 -13
- {samara-0.2 → samara-0.4}/src/samara/utils/logger.py +20 -8
- {samara-0.2 → samara-0.4}/src/samara/workflow/actions/base.py +7 -4
- {samara-0.2 → samara-0.4}/src/samara/workflow/actions/http.py +9 -4
- {samara-0.2 → samara-0.4}/src/samara/workflow/controller.py +0 -1
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/hooks.py +28 -8
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_extract.py +2 -2
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_load.py +5 -3
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_transform.py +0 -1
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/extract.py +21 -48
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/job.py +9 -10
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/load.py +22 -49
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/schema.py +19 -14
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/session.py +54 -5
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transform.py +13 -40
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/base.py +8 -8
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/filter.py +1 -7
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/join.py +4 -23
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/select.py +0 -5
- samara-0.2/src/samara/alert/__init__.py +0 -16
- samara-0.2/src/samara/alert/channels/__init__.py +0 -30
- samara-0.2/src/samara/alert/channels/base.py +0 -124
- samara-0.2/src/samara/alert/channels/email.py +0 -135
- samara-0.2/src/samara/alert/channels/file.py +0 -101
- samara-0.2/src/samara/alert/channels/http.py +0 -146
- samara-0.2/src/samara/alert/controller.py +0 -219
- samara-0.2/src/samara/alert/rules/__init__.py +0 -28
- samara-0.2/src/samara/alert/rules/base.py +0 -86
- samara-0.2/src/samara/alert/rules/env_vars_matches.py +0 -120
- samara-0.2/src/samara/alert/rules/exception_regex.py +0 -94
- samara-0.2/src/samara/alert/template.py +0 -103
- samara-0.2/src/samara/alert/trigger.py +0 -155
- {samara-0.2 → samara-0.4}/LICENSE +0 -0
- {samara-0.2 → samara-0.4}/src/samara/__main__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/telemetry.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/types.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/utils/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/actions/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/actions/move_or_copy_job_files.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/model_job.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_aggregate.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_cast.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_distinct.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_drop.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_dropduplicates.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_dropna.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_filter.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_groupby.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_join.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_orderby.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_pivot.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_select.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/models/transforms/model_withcolumn.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/polars/.gitkeep +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/__init__.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/aggregate.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/cast.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/distinct.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/drop.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/dropduplicates.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/dropna.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/groupby.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/orderby.py +0 -0
- {samara-0.2 → samara-0.4}/src/samara/workflow/jobs/spark/transforms/pivot.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
|
121
|
+
"""Validate workflow configuration files.
|
|
147
122
|
|
|
148
|
-
Load and validate
|
|
149
|
-
|
|
150
|
-
fail-fast validation
|
|
151
|
-
|
|
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
|
|
178
|
+
"""Execute the workflow.
|
|
264
179
|
|
|
265
|
-
Load 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
"""Raise when
|
|
113
|
+
class SamaraWorkflowError(SamaraError):
|
|
114
|
+
"""Raise when ETL job execution fails.
|
|
135
115
|
|
|
136
|
-
|
|
137
|
-
including
|
|
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
|
|
124
|
+
message: Description of the job execution error
|
|
145
125
|
"""
|
|
146
|
-
super().__init__(message=message, exit_code=ExitCode.
|
|
126
|
+
super().__init__(message=message, exit_code=ExitCode.JOB_ERROR)
|
|
147
127
|
|
|
148
128
|
|
|
149
|
-
class
|
|
150
|
-
"""Raise when
|
|
129
|
+
class SamaraActionError(SamaraError):
|
|
130
|
+
"""Raise when a lifecycle hook action fails to execute.
|
|
151
131
|
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
141
|
+
message: Description of the action execution error
|
|
161
142
|
"""
|
|
162
|
-
super().__init__(message=message, exit_code=ExitCode.
|
|
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:
|
|
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:
|