easy-data-loader 0.1.4__tar.gz → 0.1.6__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.
- {easy_data_loader-0.1.4/src/easy_data_loader.egg-info → easy_data_loader-0.1.6}/PKG-INFO +18 -10
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/README.md +17 -9
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/pyproject.toml +1 -1
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/cli.py +159 -86
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/config_loader.py +88 -18
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/models.py +63 -7
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/orchestrator.py +13 -14
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/pipeline.py +35 -9
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/pipeline_base.py +9 -16
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/procedure_pipeline.py +8 -2
- easy_data_loader-0.1.6/src/easy_data_loader/resource_access.py +58 -0
- easy_data_loader-0.1.6/src/easy_data_loader/utils.py +17 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6/src/easy_data_loader.egg-info}/PKG-INFO +18 -10
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/SOURCES.txt +11 -0
- easy_data_loader-0.1.6/tests/test_cli.py +196 -0
- easy_data_loader-0.1.6/tests/test_config_loader.py +143 -0
- easy_data_loader-0.1.6/tests/test_database_connector.py +36 -0
- easy_data_loader-0.1.6/tests/test_file_operations.py +89 -0
- easy_data_loader-0.1.6/tests/test_imports.py +24 -0
- easy_data_loader-0.1.6/tests/test_models.py +110 -0
- easy_data_loader-0.1.6/tests/test_pipeline_transform_order.py +83 -0
- easy_data_loader-0.1.6/tests/test_procedure_execution_mode.py +92 -0
- easy_data_loader-0.1.6/tests/test_resource_access.py +179 -0
- easy_data_loader-0.1.6/tests/test_source_file_delete.py +122 -0
- easy_data_loader-0.1.4/tests/test_imports.py +0 -10
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/LICENSE +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/setup.cfg +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/__init__.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/custom_exceptions.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/data_inferrence.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/database_connector.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/database_operations.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/driver_detector.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/file_operations.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/log.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/dependency_links.txt +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/entry_points.txt +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/requires.txt +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/top_level.txt +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/tests/test_data_inference.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/tests/test_orchestrator.py +0 -0
- {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easy_data_loader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Data transfer utilities between files and databases
|
|
5
5
|
Author-email: Bojoi Gabriel <bojoigabriel@gmail.com>
|
|
6
6
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -61,16 +61,26 @@ uv add easy_data_loader
|
|
|
61
61
|
easy-data-loader init
|
|
62
62
|
```
|
|
63
63
|
2. Review the generated `config/` folders for sample resources and pipelines.
|
|
64
|
-
3.
|
|
64
|
+
3. List all discovered resources and pipelines:
|
|
65
65
|
```bash
|
|
66
|
-
easy-data-loader
|
|
66
|
+
easy-data-loader list
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## ✔️ Generic concepts
|
|
70
70
|
|
|
71
71
|
`easy_data_loader` uses `resources` as a way to define a file or a database. The resouces can represent either a source or a destination making posible the folowing ETL scenarios: file -> file, file -> database, database -> file, database -> database.
|
|
72
72
|
|
|
73
|
-
`easy_data_loader` project initializer will
|
|
73
|
+
`easy_data_loader` project initializer will create the predefined folder structure `/config/resources`. Resource `.env` files are detected from their contents: database resources define `CONN_SERVER_TYPE`, while folder resources define `FOLDER_PATH`.
|
|
74
|
+
|
|
75
|
+
Individual file definitions are regular Python objects in modules under `/config/resources`. They are declared as a flat dictionary, independent of any folder resource - the folder is only chosen when a pipeline references the file:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
FILES = {
|
|
79
|
+
"example_file": FileEntry(file_type=FileType.CSV, file_name="data"),
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A pipeline (or `resource_access` helper) references the file by combining a folder resource name with the file key: `"example_folder.example_file"`.
|
|
74
84
|
|
|
75
85
|
A secondary predefined folder `/config/pipelines` will contain the pipeline definition files, which are regular Python files. There are 3 types of pipelines that can be defined:
|
|
76
86
|
- `LoadPipeline` the main pipeline type which transports data from source to destination
|
|
@@ -80,8 +90,8 @@ A secondary predefined folder `/config/pipelines` will contain the pipeline defi
|
|
|
80
90
|
## LoadPipeline
|
|
81
91
|
|
|
82
92
|
In order to define a `LoadPipeline` we must use the `BasePipelineDefinition` from `easy_data_loader` as depicted in the example pipelines created by the initializer.
|
|
93
|
+
`pipeline_name` is never set by the user - it is derived automatically from the pipeline's file name at load time.
|
|
83
94
|
In the simplest form there are only a few mandatory parameters:
|
|
84
|
-
- `pipeline_name : str` - this name will be used to execute the pipeline
|
|
85
95
|
- `source : str` - the file name (without extension) coresponding to the desired resource to be the data source
|
|
86
96
|
- `destination : str` - the file name (without extension) coresponding to the desired resource to be the data destination
|
|
87
97
|
|
|
@@ -99,15 +109,13 @@ There are many other aspects of the pipeline that can be defined:
|
|
|
99
109
|
## ProcedurePipeline
|
|
100
110
|
|
|
101
111
|
This secondary pipeline type is responsible for executing one or more stored procedures inside a database.
|
|
102
|
-
To define one we need to use the `ProcedureDefinition` with the following parameters:
|
|
103
|
-
- `pipeline_name : str` - this name will be used to execute the pipeline
|
|
112
|
+
To define one we need to use the `ProcedureDefinition` with the following parameters (`pipeline_name` is derived automatically from the file name, same as `LoadPipeline`):
|
|
104
113
|
- `audit : str, optional` - database resource name where the audit info will be recorded
|
|
105
114
|
- `resource : str` - database resource name where the stored procedure(s) wil be executed
|
|
106
115
|
- `procedures : List[tuple(str, Optional[Dict[str, Any]])]` - list of one or more stord procedures along with optional procedures parameters as dictionaries
|
|
107
116
|
|
|
108
117
|
## OrchestratorPipeline
|
|
109
118
|
|
|
110
|
-
This pipeline type is responsible of executing sequentially a set of pipelines, `LoadPipeline`s and / or `ProcedurePipeline`s. Very simple to define using the `OrchestratorDefinition` with:
|
|
111
|
-
- `
|
|
112
|
-
- 'pipelines : List[str]` - list of pipelines to execute sequentially
|
|
119
|
+
This pipeline type is responsible of executing sequentially a set of pipelines, `LoadPipeline`s and / or `ProcedurePipeline`s. Very simple to define using the `OrchestratorDefinition` (`pipeline_name` is derived automatically from the file name) with:
|
|
120
|
+
- `pipelines : List[str]` - list of pipelines to execute sequentially
|
|
113
121
|
- `fail_fast : bool, Default True` - if any of the pipelines fail the rest of the pipelines in the list do not get executed
|
|
@@ -32,16 +32,26 @@ uv add easy_data_loader
|
|
|
32
32
|
easy-data-loader init
|
|
33
33
|
```
|
|
34
34
|
2. Review the generated `config/` folders for sample resources and pipelines.
|
|
35
|
-
3.
|
|
35
|
+
3. List all discovered resources and pipelines:
|
|
36
36
|
```bash
|
|
37
|
-
easy-data-loader
|
|
37
|
+
easy-data-loader list
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## ✔️ Generic concepts
|
|
41
41
|
|
|
42
42
|
`easy_data_loader` uses `resources` as a way to define a file or a database. The resouces can represent either a source or a destination making posible the folowing ETL scenarios: file -> file, file -> database, database -> file, database -> database.
|
|
43
43
|
|
|
44
|
-
`easy_data_loader` project initializer will
|
|
44
|
+
`easy_data_loader` project initializer will create the predefined folder structure `/config/resources`. Resource `.env` files are detected from their contents: database resources define `CONN_SERVER_TYPE`, while folder resources define `FOLDER_PATH`.
|
|
45
|
+
|
|
46
|
+
Individual file definitions are regular Python objects in modules under `/config/resources`. They are declared as a flat dictionary, independent of any folder resource - the folder is only chosen when a pipeline references the file:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
FILES = {
|
|
50
|
+
"example_file": FileEntry(file_type=FileType.CSV, file_name="data"),
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A pipeline (or `resource_access` helper) references the file by combining a folder resource name with the file key: `"example_folder.example_file"`.
|
|
45
55
|
|
|
46
56
|
A secondary predefined folder `/config/pipelines` will contain the pipeline definition files, which are regular Python files. There are 3 types of pipelines that can be defined:
|
|
47
57
|
- `LoadPipeline` the main pipeline type which transports data from source to destination
|
|
@@ -51,8 +61,8 @@ A secondary predefined folder `/config/pipelines` will contain the pipeline defi
|
|
|
51
61
|
## LoadPipeline
|
|
52
62
|
|
|
53
63
|
In order to define a `LoadPipeline` we must use the `BasePipelineDefinition` from `easy_data_loader` as depicted in the example pipelines created by the initializer.
|
|
64
|
+
`pipeline_name` is never set by the user - it is derived automatically from the pipeline's file name at load time.
|
|
54
65
|
In the simplest form there are only a few mandatory parameters:
|
|
55
|
-
- `pipeline_name : str` - this name will be used to execute the pipeline
|
|
56
66
|
- `source : str` - the file name (without extension) coresponding to the desired resource to be the data source
|
|
57
67
|
- `destination : str` - the file name (without extension) coresponding to the desired resource to be the data destination
|
|
58
68
|
|
|
@@ -70,15 +80,13 @@ There are many other aspects of the pipeline that can be defined:
|
|
|
70
80
|
## ProcedurePipeline
|
|
71
81
|
|
|
72
82
|
This secondary pipeline type is responsible for executing one or more stored procedures inside a database.
|
|
73
|
-
To define one we need to use the `ProcedureDefinition` with the following parameters:
|
|
74
|
-
- `pipeline_name : str` - this name will be used to execute the pipeline
|
|
83
|
+
To define one we need to use the `ProcedureDefinition` with the following parameters (`pipeline_name` is derived automatically from the file name, same as `LoadPipeline`):
|
|
75
84
|
- `audit : str, optional` - database resource name where the audit info will be recorded
|
|
76
85
|
- `resource : str` - database resource name where the stored procedure(s) wil be executed
|
|
77
86
|
- `procedures : List[tuple(str, Optional[Dict[str, Any]])]` - list of one or more stord procedures along with optional procedures parameters as dictionaries
|
|
78
87
|
|
|
79
88
|
## OrchestratorPipeline
|
|
80
89
|
|
|
81
|
-
This pipeline type is responsible of executing sequentially a set of pipelines, `LoadPipeline`s and / or `ProcedurePipeline`s. Very simple to define using the `OrchestratorDefinition` with:
|
|
82
|
-
- `
|
|
83
|
-
- 'pipelines : List[str]` - list of pipelines to execute sequentially
|
|
90
|
+
This pipeline type is responsible of executing sequentially a set of pipelines, `LoadPipeline`s and / or `ProcedurePipeline`s. Very simple to define using the `OrchestratorDefinition` (`pipeline_name` is derived automatically from the file name) with:
|
|
91
|
+
- `pipelines : List[str]` - list of pipelines to execute sequentially
|
|
84
92
|
- `fail_fast : bool, Default True` - if any of the pipelines fail the rest of the pipelines in the list do not get executed
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
|
+
from .utils import to_snake_case
|
|
4
5
|
|
|
5
6
|
# Integrated templates
|
|
6
7
|
PIPELINE_TEMPLATE = """
|
|
7
|
-
from easy_data_loader.pipeline import LoadPipeline
|
|
8
8
|
from easy_data_loader.models import BasePipelineDefinition, ColumnDefinition
|
|
9
9
|
import pandas as pd
|
|
10
|
-
from sqlalchemy.types import DATETIME, INT, DECIMAL, NVARCHAR
|
|
11
10
|
|
|
12
11
|
example_pipeline = BasePipelineDefinition(
|
|
13
|
-
pipeline_name="test_pipeline", # pipeline name to be used when initializing the Pipeline object
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
source
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
<<<<<<< HEAD
|
|
14
|
+
# if the source is a database then fill with the file name (witout extension) of the resource definition in config/resources (e.g. example_database)
|
|
15
|
+
# if the source is a file then the source name is represented by "<folder_resource_name>.<file_key>" -> the folder resource
|
|
16
|
+
=======
|
|
17
|
+
# source name represented by "<folder_resource_name>.<file_key>" -> the folder resource
|
|
18
|
+
>>>>>>> main
|
|
19
|
+
# corresponds to a file in config/resources (e.g. example_folder.env) and the file_key
|
|
20
|
+
# is defined in config/resources/files.py
|
|
21
|
+
source = "example_folder.example_file",
|
|
22
|
+
|
|
23
|
+
# [optional] only if the source is a database then here we define the table name for a 1:1 copy or a custom sql query
|
|
19
24
|
# source_sql = "SELECT TOP 100 * FROM dbo.FactSales"
|
|
20
25
|
|
|
21
|
-
#
|
|
26
|
+
# fill with the same logic as source
|
|
22
27
|
destination="example_database",
|
|
23
28
|
|
|
24
29
|
# if the destination is a database then here we define the destination table name
|
|
25
|
-
destination_table="
|
|
30
|
+
destination_table="example_table",
|
|
26
31
|
|
|
27
32
|
# columns definition if we are sending data to a database table
|
|
28
33
|
columns={
|
|
@@ -35,13 +40,16 @@ example_pipeline = BasePipelineDefinition(
|
|
|
35
40
|
"raw_notes" : ColumnDefinition(target_name="notes", data_type=NVARCHAR(100))
|
|
36
41
|
},
|
|
37
42
|
|
|
38
|
-
# different parameters passed to the write functions
|
|
43
|
+
# different parameters passed to the write functions to be forwarded to the pandas function that will write to destination (e.g. to_sql, to_csv, to_excel, etc.)
|
|
39
44
|
write_parameters={"if_exists" : "replace", "index" : False},
|
|
40
45
|
|
|
41
|
-
# different parameters passed to the read function
|
|
46
|
+
# different parameters passed to the read function to be forwarded to the pandas function that will read from source (e.g. read_csv, read_excel, read_sql etc.)
|
|
42
47
|
read_parameters={"sep" : ";"}
|
|
43
48
|
)
|
|
44
49
|
|
|
50
|
+
# we can define custom transformations that will be applied to the DataFrame before writing to the destination.
|
|
51
|
+
# the function must take a DataFrame as input and return a DataFrame as output.
|
|
52
|
+
|
|
45
53
|
def add_timestamp(df):
|
|
46
54
|
# Adding an audit column during load
|
|
47
55
|
df['insert_timestamp'] = pd.Timestamp.now()
|
|
@@ -54,7 +62,7 @@ PROCEDURE_TEMPLATE = """
|
|
|
54
62
|
from easy_data_loader.models import ProcedureDefinition
|
|
55
63
|
|
|
56
64
|
example_procedure = ProcedureDefinition(
|
|
57
|
-
|
|
65
|
+
|
|
58
66
|
resource="example_database",
|
|
59
67
|
procedures=[
|
|
60
68
|
("dbo.sp_UpdateSales", {"year": 2024}),
|
|
@@ -67,7 +75,6 @@ ORCHESTRATOR_TEMPLATE = """
|
|
|
67
75
|
from easy_data_loader.models import OrchestratorDefinition
|
|
68
76
|
|
|
69
77
|
example_orchestrator = OrchestratorDefinition(
|
|
70
|
-
orchestrator_name="example_orchestrator",
|
|
71
78
|
pipelines=[
|
|
72
79
|
"example_pipeline",
|
|
73
80
|
"example_procedure"
|
|
@@ -78,7 +85,8 @@ example_orchestrator = OrchestratorDefinition(
|
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
DATABASE_ENV = """
|
|
81
|
-
# database resource definition
|
|
88
|
+
# database resource definition (type is auto-detected from CONN_SERVER_TYPE)
|
|
89
|
+
# database resource definition (type is auto-detected from CONN_SERVER_TYPE)
|
|
82
90
|
CONN_SERVER_TYPE=MSSQL
|
|
83
91
|
CONN_SERVER=.
|
|
84
92
|
CONN_DATABASE=test_database
|
|
@@ -87,23 +95,48 @@ CONN_PASSWORD=my_password
|
|
|
87
95
|
CONN_PORT=1433
|
|
88
96
|
"""
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
FOLDER_PATH=./data/imports
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
FOLDER_ENV = """
|
|
99
|
+
# folder resource definition
|
|
100
|
+
# environment-specific can be absolute path or a relative path to the project root
|
|
101
|
+
FOLDER_PATH=./data/imports
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
FILES_TEMPLATE = """
|
|
105
|
+
from easy_data_loader.models import FileEntry, FileType
|
|
106
|
+
|
|
107
|
+
# File definitions are flat and independent of any folder resource. A folder is
|
|
108
|
+
# only chosen when a pipeline references the file as
|
|
109
|
+
# "<folder_resource_name>.<key>", e.g. "example_folder.example_file"
|
|
110
|
+
|
|
111
|
+
FILES = {
|
|
112
|
+
"example_file": FileEntry(
|
|
113
|
+
# supported values: FileType.CSV, FileType.XLSX, FileType.ORC, FileType.PARQUET
|
|
114
|
+
file_type=FileType.CSV,
|
|
115
|
+
|
|
116
|
+
# if the file name never changes we can use an exact name without the extension
|
|
117
|
+
file_name="large_sales_data",
|
|
118
|
+
|
|
119
|
+
# if the file name changes we can use a pattern in glob format; if multiple files matching the pattern, the most recent one will be used
|
|
120
|
+
# file_pattern="*large_sales*",
|
|
121
|
+
),
|
|
122
|
+
}
|
|
96
123
|
"""
|
|
97
124
|
|
|
98
125
|
MAIN = """
|
|
99
|
-
from easy_data_loader import LoadPipeline, ProcedurePipeline
|
|
126
|
+
from easy_data_loader import LoadPipeline, ProcedurePipeline, OrchestratorPipeline
|
|
127
|
+
|
|
128
|
+
# example execution of different types of pipelines using the easy data loader library
|
|
100
129
|
|
|
101
130
|
def main():
|
|
102
131
|
# Run an ETL pipeline
|
|
103
|
-
LoadPipeline(pipeline_name="example_pipeline").run()
|
|
132
|
+
first_step = LoadPipeline(pipeline_name="example_pipeline").run()
|
|
104
133
|
|
|
105
134
|
# Run a procedure pipeline
|
|
106
|
-
|
|
135
|
+
if first_step:
|
|
136
|
+
ProcedurePipeline(pipeline_name="example_procedure").run()
|
|
137
|
+
|
|
138
|
+
# Run an orchestrator pipeline
|
|
139
|
+
OrchestratorPipeline(pipeline_name="example_orchestrator").run()
|
|
107
140
|
|
|
108
141
|
if __name__ == "__main__":
|
|
109
142
|
main()
|
|
@@ -128,12 +161,13 @@ def init():
|
|
|
128
161
|
|
|
129
162
|
# Example files to create
|
|
130
163
|
files = {
|
|
131
|
-
"config/pipelines/
|
|
132
|
-
"config/pipelines/
|
|
133
|
-
"config/pipelines/
|
|
134
|
-
"config/resources/
|
|
135
|
-
"config/resources/
|
|
136
|
-
"
|
|
164
|
+
"config/pipelines/pipeline.py.example": PIPELINE_TEMPLATE,
|
|
165
|
+
"config/pipelines/procedure.py.example": PROCEDURE_TEMPLATE,
|
|
166
|
+
"config/pipelines/orchestrator.py.example": ORCHESTRATOR_TEMPLATE,
|
|
167
|
+
"config/resources/database.env.example": DATABASE_ENV,
|
|
168
|
+
"config/resources/folder.env.example": FOLDER_ENV,
|
|
169
|
+
"config/resources/files.py": FILES_TEMPLATE,
|
|
170
|
+
"pipelines_start.py": MAIN,
|
|
137
171
|
}
|
|
138
172
|
|
|
139
173
|
for name, content in files.items():
|
|
@@ -159,6 +193,12 @@ def list():
|
|
|
159
193
|
for name in config.get_all_resources():
|
|
160
194
|
click.echo(f" - {name}")
|
|
161
195
|
|
|
196
|
+
click.echo(
|
|
197
|
+
"\n--- Discovered File Entries (combine with any folder as <folder>.<key>) ---"
|
|
198
|
+
)
|
|
199
|
+
for name in config.get_all_file_entries():
|
|
200
|
+
click.echo(f" - {name}")
|
|
201
|
+
|
|
162
202
|
click.echo("\n--- Discovered Pipelines ---")
|
|
163
203
|
for name in config.get_all_pipelines():
|
|
164
204
|
click.echo(f" - {name}")
|
|
@@ -167,7 +207,7 @@ def list():
|
|
|
167
207
|
@main.command()
|
|
168
208
|
@click.argument("resource_name")
|
|
169
209
|
@click.argument("table_name")
|
|
170
|
-
def
|
|
210
|
+
def inspect_database_table(resource_name, table_name):
|
|
171
211
|
"""Inspect a database table and generate ColumnDefinition code"""
|
|
172
212
|
from .config_loader import Configuration
|
|
173
213
|
from .database_connector import CONNECTOR_FACTORY
|
|
@@ -204,70 +244,38 @@ def inspect_db(resource_name, table_name):
|
|
|
204
244
|
|
|
205
245
|
|
|
206
246
|
@main.command()
|
|
207
|
-
|
|
208
|
-
|
|
247
|
+
@click.argument("resource_name")
|
|
248
|
+
def inspect_file_resource(resource_name):
|
|
249
|
+
"""Inspect a file resource and generate ColumnDefinition code"""
|
|
209
250
|
from .config_loader import Configuration
|
|
210
|
-
from .
|
|
211
|
-
|
|
212
|
-
OrchestratorDefinition,
|
|
213
|
-
ProcedureDefinition,
|
|
214
|
-
)
|
|
215
|
-
from .pipeline import LoadPipeline
|
|
216
|
-
from .procedure_pipeline import ProcedurePipeline
|
|
251
|
+
from .file_operations import FileOperations
|
|
252
|
+
from .models import FileSettings, FileType
|
|
217
253
|
|
|
218
254
|
config = Configuration()
|
|
219
|
-
|
|
255
|
+
resource = config.get_resource(resource_name)
|
|
220
256
|
|
|
221
|
-
if not
|
|
222
|
-
click.echo("
|
|
257
|
+
if not isinstance(resource, FileSettings):
|
|
258
|
+
click.echo(f"Error: Resource '{resource_name}' is not a file resource.")
|
|
223
259
|
return
|
|
224
260
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
click.echo(f"🚀 Running {len(pipelines)} discovered pipelines...\n")
|
|
228
|
-
|
|
229
|
-
for name in pipelines:
|
|
230
|
-
click.echo(f"Pipeline: {name} ... ", nl=False)
|
|
231
|
-
try:
|
|
232
|
-
definition = config.get_pipeline(name)
|
|
233
|
-
if isinstance(definition, BasePipelineDefinition):
|
|
234
|
-
success = LoadPipeline(name).run()
|
|
235
|
-
elif isinstance(definition, ProcedureDefinition):
|
|
236
|
-
success = ProcedurePipeline(name).run()
|
|
237
|
-
elif isinstance(definition, OrchestratorDefinition):
|
|
238
|
-
from .orchestrator import OrchestratorPipeline
|
|
239
|
-
|
|
240
|
-
success = OrchestratorPipeline(name).run()
|
|
241
|
-
else:
|
|
242
|
-
success = False
|
|
243
|
-
|
|
244
|
-
results[name] = "SUCCESS" if success else "FAILED"
|
|
245
|
-
except Exception as e:
|
|
246
|
-
results[name] = f"ERROR: {str(e)}"
|
|
247
|
-
|
|
248
|
-
click.echo(results[name])
|
|
249
|
-
|
|
250
|
-
click.echo("\n" + "=" * 40)
|
|
251
|
-
click.echo(f"{'PIPELINE':<25} | {'STATUS'}")
|
|
252
|
-
click.echo("-" * 40)
|
|
253
|
-
for name, status in results.items():
|
|
254
|
-
click.echo(f"{name:<25} | {status}")
|
|
261
|
+
ops = FileOperations(resource)
|
|
262
|
+
df = ops.read_file()
|
|
255
263
|
|
|
264
|
+
schema = ops._inferencers[ops.file_type](
|
|
265
|
+
df if ops.file_type in [FileType.CSV, FileType.XLSX] else ops.file_path
|
|
266
|
+
)
|
|
256
267
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"""Run a specific orchestrator by name"""
|
|
261
|
-
from .orchestrator import OrchestratorPipeline
|
|
268
|
+
if not schema:
|
|
269
|
+
click.echo(f"No columns found for resource '{resource_name}'.")
|
|
270
|
+
return
|
|
262
271
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
click.echo(f"💥 Error: {str(e)}")
|
|
272
|
+
click.echo(f"\n# Suggested Column definitions for {resource_name}:")
|
|
273
|
+
click.echo("columns={")
|
|
274
|
+
for col, dtype in schema.items():
|
|
275
|
+
click.echo(
|
|
276
|
+
f' "{col}": ColumnDefinition(target_name="{to_snake_case(col)}", data_type={dtype}),'
|
|
277
|
+
)
|
|
278
|
+
click.echo("}")
|
|
271
279
|
|
|
272
280
|
|
|
273
281
|
@main.command()
|
|
@@ -275,7 +283,7 @@ def validate_resources():
|
|
|
275
283
|
"""Validate all configured resources"""
|
|
276
284
|
from .config_loader import Configuration
|
|
277
285
|
from .database_connector import CONNECTOR_FACTORY
|
|
278
|
-
from .models import FileSettings
|
|
286
|
+
from .models import FileSettings, FolderSettings
|
|
279
287
|
|
|
280
288
|
config = Configuration()
|
|
281
289
|
resources = config.get_all_resources()
|
|
@@ -306,6 +314,12 @@ def validate_resources():
|
|
|
306
314
|
results[name] = "OK (Path Exists)"
|
|
307
315
|
else:
|
|
308
316
|
raise ValueError(f"Path does not exist: {resource.folder_path}")
|
|
317
|
+
elif isinstance(resource, FolderSettings):
|
|
318
|
+
# Validate Folder Path
|
|
319
|
+
if resource.folder_path.exists():
|
|
320
|
+
results[name] = "OK (Path Exists)"
|
|
321
|
+
else:
|
|
322
|
+
raise ValueError(f"Path does not exist: {resource.folder_path}")
|
|
309
323
|
else:
|
|
310
324
|
results[name] = "UNKNOWN TYPE"
|
|
311
325
|
|
|
@@ -321,5 +335,64 @@ def validate_resources():
|
|
|
321
335
|
click.echo(f"{name:<30} | {status}")
|
|
322
336
|
|
|
323
337
|
|
|
338
|
+
@main.command()
|
|
339
|
+
def validate_pipelines():
|
|
340
|
+
"""Validate pipeline instantiation and their connected resources (including folder.file_key combinations)"""
|
|
341
|
+
from typing import Optional, Union
|
|
342
|
+
|
|
343
|
+
from .config_loader import Configuration
|
|
344
|
+
from .models import (
|
|
345
|
+
BasePipelineDefinition,
|
|
346
|
+
OrchestratorDefinition,
|
|
347
|
+
ProcedureDefinition,
|
|
348
|
+
)
|
|
349
|
+
from .orchestrator import OrchestratorPipeline
|
|
350
|
+
from .pipeline import LoadPipeline
|
|
351
|
+
from .procedure_pipeline import ProcedurePipeline
|
|
352
|
+
|
|
353
|
+
config = Configuration()
|
|
354
|
+
pipelines = config.get_all_pipelines()
|
|
355
|
+
|
|
356
|
+
if not pipelines:
|
|
357
|
+
click.echo("No pipelines found.")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
click.echo(f"🔍 Validating {len(pipelines)} pipelines...\n")
|
|
361
|
+
|
|
362
|
+
results = {}
|
|
363
|
+
|
|
364
|
+
for name, definition in pipelines.items():
|
|
365
|
+
click.echo(f"Pipeline: {name} ... ", nl=False)
|
|
366
|
+
instance: Optional[Union[LoadPipeline, ProcedurePipeline]] = None
|
|
367
|
+
try:
|
|
368
|
+
if isinstance(definition, BasePipelineDefinition):
|
|
369
|
+
instance = LoadPipeline(name)
|
|
370
|
+
elif isinstance(definition, ProcedureDefinition):
|
|
371
|
+
instance = ProcedurePipeline(name)
|
|
372
|
+
elif isinstance(definition, OrchestratorDefinition):
|
|
373
|
+
OrchestratorPipeline(name)
|
|
374
|
+
# instantiation alone does not touch nested pipelines, so validate them too
|
|
375
|
+
for nested_name in definition.pipelines:
|
|
376
|
+
config.get_pipeline(nested_name)
|
|
377
|
+
else:
|
|
378
|
+
raise ValueError(f"Unknown pipeline type: {type(definition)}")
|
|
379
|
+
|
|
380
|
+
results[name] = "OK"
|
|
381
|
+
except Exception as e:
|
|
382
|
+
results[name] = f"FAILED: {str(e)}"
|
|
383
|
+
finally:
|
|
384
|
+
# dispose any database connectors opened during instantiation
|
|
385
|
+
if instance is not None:
|
|
386
|
+
instance._cleanup()
|
|
387
|
+
|
|
388
|
+
click.echo(results[name])
|
|
389
|
+
|
|
390
|
+
click.echo("\n" + "=" * 60)
|
|
391
|
+
click.echo(f"{'PIPELINE':<30} | {'STATUS'}")
|
|
392
|
+
click.echo("-" * 60)
|
|
393
|
+
for name, status in results.items():
|
|
394
|
+
click.echo(f"{name:<30} | {status}")
|
|
395
|
+
|
|
396
|
+
|
|
324
397
|
if __name__ == "__main__":
|
|
325
398
|
main()
|