easy-data-loader 0.1.7__tar.gz → 0.1.8__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.7/src/easy_data_loader.egg-info → easy_data_loader-0.1.8}/PKG-INFO +1 -1
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/pyproject.toml +1 -1
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/file_operations.py +21 -14
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/pipeline.py +21 -9
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8/src/easy_data_loader.egg-info}/PKG-INFO +1 -1
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_file_operations.py +12 -7
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_validation.py +60 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/LICENSE +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/README.md +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/setup.cfg +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/__init__.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/cli.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/config_loader.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/custom_exceptions.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/data_inferrence.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/database_connector.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/database_operations.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/driver_detector.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/log.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/models.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/orchestrator.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/pipeline_base.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/procedure_pipeline.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/resource_access.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/utils.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/SOURCES.txt +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/dependency_links.txt +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/entry_points.txt +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/requires.txt +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/top_level.txt +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_cli.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_config_loader.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_data_inference.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_database_connector.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_imports.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_models.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_orchestrator.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_pipeline_transform_order.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_procedure_execution_mode.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_resource_access.py +0 -0
- {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_source_file_delete.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Callable, Dict, Optional, Union
|
|
2
|
+
from typing import Callable, Dict, Optional, Union, Tuple
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from sqlalchemy import types
|
|
5
5
|
|
|
@@ -16,6 +16,8 @@ class FileOperations(LoggedComponent):
|
|
|
16
16
|
self.settings = settings
|
|
17
17
|
self.file_type = self.settings.file_type
|
|
18
18
|
self.file_path: Optional[Path] = None
|
|
19
|
+
if self.settings.file_name:
|
|
20
|
+
self.file_name_invalid = self.settings.file_name + "_invalid"
|
|
19
21
|
|
|
20
22
|
# mapping the file reader by extension
|
|
21
23
|
self._readers: Dict[FileType, Callable] = {
|
|
@@ -46,7 +48,7 @@ class FileOperations(LoggedComponent):
|
|
|
46
48
|
Identifies the file based on pattern (latest) or explicit name.
|
|
47
49
|
"""
|
|
48
50
|
if self.settings.file_pattern:
|
|
49
|
-
files = list(self.settings.folder_path.glob(self.settings.file_pattern))
|
|
51
|
+
files = list(self.settings.folder_path.glob('*' + self.settings.file_pattern + '*'))
|
|
50
52
|
if not files:
|
|
51
53
|
self.log_and_raise(
|
|
52
54
|
ValueError,
|
|
@@ -82,26 +84,21 @@ class FileOperations(LoggedComponent):
|
|
|
82
84
|
)
|
|
83
85
|
raise
|
|
84
86
|
|
|
85
|
-
def write_file(self, df: pd.DataFrame, **kwargs) -> Path:
|
|
87
|
+
def write_file(self, df: pd.DataFrame, path: Path, **kwargs) -> Path:
|
|
86
88
|
"""
|
|
87
|
-
Ensures a valid path exists and writes the dataframe.
|
|
88
89
|
If file_name is provided, it uses it (and overwrites).
|
|
89
90
|
If file_pattern is provided, it constructs a name using the pattern and a timestamp.
|
|
90
91
|
"""
|
|
91
92
|
|
|
92
|
-
output_path = self._construct_output_path()
|
|
93
93
|
try:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.logger.info(f"Writing to file: {output_path.name}")
|
|
97
|
-
self._writers[self.file_type](df, output_path, **kwargs)
|
|
94
|
+
self._writers[self.file_type](df, path, **kwargs)
|
|
98
95
|
|
|
99
|
-
return
|
|
96
|
+
return path
|
|
100
97
|
except Exception as e:
|
|
101
|
-
self.log_exception(e, f"Failed to write to: {
|
|
98
|
+
self.log_exception(e, f"Failed to write to: {path}")
|
|
102
99
|
raise
|
|
103
100
|
|
|
104
|
-
def _construct_output_path(self) -> Path:
|
|
101
|
+
def _construct_output_path(self) -> Tuple[Path, Path]:
|
|
105
102
|
"""
|
|
106
103
|
Construct the output file path based on the settings.
|
|
107
104
|
If the file name is defined the output path will be fixed,
|
|
@@ -112,11 +109,21 @@ class FileOperations(LoggedComponent):
|
|
|
112
109
|
|
|
113
110
|
if self.settings.file_name:
|
|
114
111
|
base_path: Path = self.settings.folder_path / self.settings.file_name
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
invalid_base_path: Path = self.settings.folder_path / self.file_name_invalid
|
|
113
|
+
|
|
114
|
+
return base_path.with_suffix(file_extension), invalid_base_path.with_suffix(
|
|
115
|
+
file_extension
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
valid_pattern = (
|
|
117
119
|
self.settings.folder_path
|
|
118
120
|
/ f"{self.settings.file_pattern}_{timestamp}{file_extension}"
|
|
119
121
|
)
|
|
122
|
+
invalid_pattern = (
|
|
123
|
+
self.settings.folder_path
|
|
124
|
+
/ f"{self.settings.file_pattern}_invalid_{timestamp}{file_extension}"
|
|
125
|
+
)
|
|
126
|
+
return valid_pattern, invalid_pattern
|
|
120
127
|
|
|
121
128
|
def _apply_file_preprocessor(self, preprocessor_func: Callable[[Path], Path]):
|
|
122
129
|
"""
|
|
@@ -290,12 +290,15 @@ class LoadPipeline(BasePipeline):
|
|
|
290
290
|
|
|
291
291
|
current_timestamp = pd.Timestamp.now()
|
|
292
292
|
|
|
293
|
-
invalid = pd.DataFrame(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
293
|
+
invalid = pd.DataFrame(
|
|
294
|
+
{
|
|
295
|
+
"orchestrator_id": str(self.orchestrator_id),
|
|
296
|
+
"pipeline_id": str(self.pipeline_id),
|
|
297
|
+
"pipeline_name": self.definition.pipeline_name,
|
|
298
|
+
"error": invalid_error_messages,
|
|
299
|
+
"insert_timestamp": current_timestamp,
|
|
300
|
+
}
|
|
301
|
+
)
|
|
299
302
|
|
|
300
303
|
return valid, invalid
|
|
301
304
|
|
|
@@ -319,13 +322,22 @@ class LoadPipeline(BasePipeline):
|
|
|
319
322
|
table_name=self.definition.destination_table_invalid,
|
|
320
323
|
df=invalid,
|
|
321
324
|
index=False,
|
|
322
|
-
if_exists="replace"
|
|
325
|
+
if_exists="replace",
|
|
323
326
|
)
|
|
324
327
|
elif self.dst_file_ops: # File destination
|
|
325
|
-
self.dst_file_ops.
|
|
328
|
+
valid_path, invalid_path = self.dst_file_ops._construct_output_path()
|
|
329
|
+
|
|
330
|
+
valid_path.parent.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
|
|
332
|
+
self.logger.info(f"Writing to file: {valid_path.name}")
|
|
333
|
+
self.dst_file_ops.write_file(
|
|
334
|
+
df, valid_path, **self.definition.write_parameters
|
|
335
|
+
)
|
|
326
336
|
if not invalid.empty:
|
|
337
|
+
invalid_path.parent.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
self.logger.info(f"Writing to file: {invalid_path.name}")
|
|
327
339
|
self.dst_file_ops.write_file(
|
|
328
|
-
invalid, **self.definition.write_parameters
|
|
340
|
+
invalid, invalid_path, **self.definition.write_parameters
|
|
329
341
|
)
|
|
330
342
|
else:
|
|
331
343
|
return False
|
|
@@ -54,7 +54,8 @@ def test_read_write_csv_round_trip():
|
|
|
54
54
|
ops = FileOperations(settings)
|
|
55
55
|
|
|
56
56
|
df = pd.DataFrame({"a": [1, 2], "b": ["x", "y"]})
|
|
57
|
-
output_path = ops.
|
|
57
|
+
output_path, _ = ops._construct_output_path()
|
|
58
|
+
ops.write_file(df, output_path)
|
|
58
59
|
|
|
59
60
|
assert output_path.exists()
|
|
60
61
|
|
|
@@ -73,8 +74,9 @@ def test_construct_output_path_uses_file_name_when_defined():
|
|
|
73
74
|
file_type=FileType.CSV, folder_path=tmpdir, file_name="fixed_output"
|
|
74
75
|
)
|
|
75
76
|
ops = FileOperations(settings)
|
|
76
|
-
|
|
77
|
-
assert
|
|
77
|
+
valid_path, invalid_path = ops._construct_output_path()
|
|
78
|
+
assert valid_path == Path(tmpdir) / "fixed_output.csv"
|
|
79
|
+
assert invalid_path == Path(tmpdir) / "fixed_output_invalid.csv"
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
def test_construct_output_path_uses_pattern_with_timestamp():
|
|
@@ -83,7 +85,10 @@ def test_construct_output_path_uses_pattern_with_timestamp():
|
|
|
83
85
|
file_type=FileType.CSV, folder_path=tmpdir, file_pattern="export"
|
|
84
86
|
)
|
|
85
87
|
ops = FileOperations(settings)
|
|
86
|
-
|
|
87
|
-
assert
|
|
88
|
-
assert
|
|
89
|
-
assert
|
|
88
|
+
valid_path, invalid_path = ops._construct_output_path()
|
|
89
|
+
assert valid_path.parent == Path(tmpdir)
|
|
90
|
+
assert valid_path.name.startswith("export_")
|
|
91
|
+
assert valid_path.suffix == ".csv"
|
|
92
|
+
assert invalid_path.parent == Path(tmpdir)
|
|
93
|
+
assert invalid_path.name.startswith("export_invalid_")
|
|
94
|
+
assert invalid_path.suffix == ".csv"
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
3
|
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
4
7
|
import pytest
|
|
5
8
|
from pydantic import BaseModel, Field
|
|
6
9
|
|
|
@@ -8,6 +11,8 @@ from easy_data_loader.config_loader import Configuration
|
|
|
8
11
|
from easy_data_loader.models import (
|
|
9
12
|
BasePipelineDefinition,
|
|
10
13
|
FileBasedConnectionSettings,
|
|
14
|
+
FileSettings,
|
|
15
|
+
FileType,
|
|
11
16
|
ServerType,
|
|
12
17
|
)
|
|
13
18
|
from easy_data_loader.pipeline import LoadPipeline
|
|
@@ -95,6 +100,61 @@ def test_validation_fail_false_keeps_valid_rows():
|
|
|
95
100
|
assert rows[0] == (1, "Alice")
|
|
96
101
|
assert rows[1] == (3, "Charlie")
|
|
97
102
|
|
|
103
|
+
cursor_dst = sqlite3.connect(dst_db_path).cursor()
|
|
104
|
+
cursor_dst.execute("SELECT error FROM target_table_invalid")
|
|
105
|
+
invalid_rows = cursor_dst.fetchall()
|
|
106
|
+
cursor_dst.connection.close()
|
|
107
|
+
|
|
108
|
+
assert len(invalid_rows) == 1
|
|
109
|
+
assert "name=Bo" in invalid_rows[0][0]
|
|
110
|
+
assert "at least 3 characters" in invalid_rows[0][0]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_validation_writes_valid_and_invalid_file_outputs():
|
|
114
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
115
|
+
src_db_path = os.path.join(tmpdir, "src.db")
|
|
116
|
+
audit_db_path = os.path.join(tmpdir, "audit.db")
|
|
117
|
+
|
|
118
|
+
conn_src = sqlite3.connect(src_db_path)
|
|
119
|
+
cursor_src = conn_src.cursor()
|
|
120
|
+
cursor_src.execute("CREATE TABLE source_table (id INTEGER, name TEXT)")
|
|
121
|
+
cursor_src.execute("INSERT INTO source_table VALUES (1, 'Alice')")
|
|
122
|
+
cursor_src.execute("INSERT INTO source_table VALUES (2, 'Bo')")
|
|
123
|
+
conn_src.commit()
|
|
124
|
+
conn_src.close()
|
|
125
|
+
|
|
126
|
+
config = Configuration()
|
|
127
|
+
config.resources["src_db"] = FileBasedConnectionSettings(
|
|
128
|
+
conn_server_type=ServerType.SQLITE, file_path=src_db_path
|
|
129
|
+
)
|
|
130
|
+
config.resources["output_file"] = FileSettings(
|
|
131
|
+
file_type=FileType.CSV, folder_path=tmpdir, file_name="output"
|
|
132
|
+
)
|
|
133
|
+
config.resources["audit_db"] = FileBasedConnectionSettings(
|
|
134
|
+
conn_server_type=ServerType.SQLITE, file_path=audit_db_path
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
config.pipelines["test_pipeline"] = BasePipelineDefinition(
|
|
138
|
+
pipeline_name="test_pipeline",
|
|
139
|
+
source="src_db",
|
|
140
|
+
source_sql="SELECT * FROM source_table",
|
|
141
|
+
destination="output_file",
|
|
142
|
+
audit="audit_db",
|
|
143
|
+
validator=ValidationTestModel,
|
|
144
|
+
validation_fail=False,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
success = LoadPipeline("test_pipeline").run()
|
|
148
|
+
|
|
149
|
+
assert success is True
|
|
150
|
+
|
|
151
|
+
valid_df = pd.read_csv(Path(tmpdir) / "output.csv")
|
|
152
|
+
invalid_df = pd.read_csv(Path(tmpdir) / "output_invalid.csv")
|
|
153
|
+
|
|
154
|
+
assert valid_df.to_dict(orient="records") == [{"id": 1, "name": "Alice"}]
|
|
155
|
+
assert invalid_df["error"].str.contains("name=Bo").all()
|
|
156
|
+
assert invalid_df["error"].str.contains("at least 3 characters").all()
|
|
157
|
+
|
|
98
158
|
|
|
99
159
|
def test_validation_fail_true_fails_pipeline():
|
|
100
160
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/database_connector.py
RENAMED
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/database_operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/procedure_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/requires.txt
RENAMED
|
File without changes
|
{easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|