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.
Files changed (42) hide show
  1. {easy_data_loader-0.1.4/src/easy_data_loader.egg-info → easy_data_loader-0.1.6}/PKG-INFO +18 -10
  2. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/README.md +17 -9
  3. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/pyproject.toml +1 -1
  4. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/cli.py +159 -86
  5. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/config_loader.py +88 -18
  6. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/models.py +63 -7
  7. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/orchestrator.py +13 -14
  8. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/pipeline.py +35 -9
  9. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/pipeline_base.py +9 -16
  10. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/procedure_pipeline.py +8 -2
  11. easy_data_loader-0.1.6/src/easy_data_loader/resource_access.py +58 -0
  12. easy_data_loader-0.1.6/src/easy_data_loader/utils.py +17 -0
  13. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6/src/easy_data_loader.egg-info}/PKG-INFO +18 -10
  14. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/SOURCES.txt +11 -0
  15. easy_data_loader-0.1.6/tests/test_cli.py +196 -0
  16. easy_data_loader-0.1.6/tests/test_config_loader.py +143 -0
  17. easy_data_loader-0.1.6/tests/test_database_connector.py +36 -0
  18. easy_data_loader-0.1.6/tests/test_file_operations.py +89 -0
  19. easy_data_loader-0.1.6/tests/test_imports.py +24 -0
  20. easy_data_loader-0.1.6/tests/test_models.py +110 -0
  21. easy_data_loader-0.1.6/tests/test_pipeline_transform_order.py +83 -0
  22. easy_data_loader-0.1.6/tests/test_procedure_execution_mode.py +92 -0
  23. easy_data_loader-0.1.6/tests/test_resource_access.py +179 -0
  24. easy_data_loader-0.1.6/tests/test_source_file_delete.py +122 -0
  25. easy_data_loader-0.1.4/tests/test_imports.py +0 -10
  26. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/LICENSE +0 -0
  27. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/setup.cfg +0 -0
  28. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/__init__.py +0 -0
  29. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/custom_exceptions.py +0 -0
  30. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/data_inferrence.py +0 -0
  31. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/database_connector.py +0 -0
  32. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/database_operations.py +0 -0
  33. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/driver_detector.py +0 -0
  34. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/file_operations.py +0 -0
  35. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader/log.py +0 -0
  36. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/dependency_links.txt +0 -0
  37. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/entry_points.txt +0 -0
  38. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/requires.txt +0 -0
  39. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/src/easy_data_loader.egg-info/top_level.txt +0 -0
  40. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/tests/test_data_inference.py +0 -0
  41. {easy_data_loader-0.1.4 → easy_data_loader-0.1.6}/tests/test_orchestrator.py +0 -0
  42. {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.4
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. Run all discovered pipelines across the active configurations:
64
+ 3. List all discovered resources and pipelines:
65
65
  ```bash
66
- easy-data-loader run_all
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 created the predefined folder structure `/config/resources` where the resources are expected to be defined following the current convention: the file type is .env and the file name must be prefixed with the resource type `file_` or `database_`. The predefined folder structure together with the naming convention enables `easy_data_loader` to find and load all resources.
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
- - `orchestrator_name : str` - name by which the orchestrator is executer
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. Run all discovered pipelines across the active configurations:
35
+ 3. List all discovered resources and pipelines:
36
36
  ```bash
37
- easy-data-loader run_all
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 created the predefined folder structure `/config/resources` where the resources are expected to be defined following the current convention: the file type is .env and the file name must be prefixed with the resource type `file_` or `database_`. The predefined folder structure together with the naming convention enables `easy_data_loader` to find and load all resources.
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
- - `orchestrator_name : str` - name by which the orchestrator is executer
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "easy_data_loader"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Data transfer utilities between files and databases"
5
5
  authors = [{ name = "Bojoi Gabriel", email = "bojoigabriel@gmail.com" }]
6
6
  readme = "README.md"
@@ -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
- # source name represented by the file having the source settings -> corresponds to a file in the config/resources folder
16
- source = "example_file",
17
-
18
- # "dbo.FactSales" ## if the source is a database then source sql defines the table name or a custom query to be executed
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
- # destination name represented by the file having the destination settings -> corresponds to a file in the config/resources folder
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="dbo.LargeSalesData",
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
- pipeline_name="example_procedure",
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
- FILE_ENV = """
91
- # file resource definition
92
- FILE_TYPE=CSV # can also be XLSX, PARQUET, ORC
93
- FOLDER_PATH=./data/imports # source folder where the file is located
94
- FILE_NAME=large_sales_data # exact file name without extension
95
- #FILE_PATTERN=large_sales # file pattern to search in the source folder
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
- ProcedurePipeline(pipeline_name="example_procedure").run()
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/pipeline_example.py": PIPELINE_TEMPLATE,
132
- "config/pipelines/procedure_example.py": PROCEDURE_TEMPLATE,
133
- "config/pipelines/orchestrator_example.py": ORCHESTRATOR_TEMPLATE,
134
- "config/resources/database_example.env": DATABASE_ENV,
135
- "config/resources/file_example.env": FILE_ENV,
136
- "main.py": MAIN,
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 inspect_db(resource_name, table_name):
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
- def run_all():
208
- """Run all discovered pipelines and show status summary"""
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 .models import (
211
- BasePipelineDefinition,
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
- pipelines = config.get_all_pipelines()
255
+ resource = config.get_resource(resource_name)
220
256
 
221
- if not pipelines:
222
- click.echo("No pipelines discovered.")
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
- results = {}
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
- @main.command()
258
- @click.argument("orchestrator_name")
259
- def run_orchestrator(orchestrator_name):
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
- try:
264
- success = OrchestratorPipeline(orchestrator_name).run()
265
- if success:
266
- click.echo(f"✅ Orchestrator '{orchestrator_name}' completed successfully.")
267
- else:
268
- click.echo(f"❌ Orchestrator '{orchestrator_name}' failed.")
269
- except Exception as e:
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()