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.
Files changed (41) hide show
  1. {easy_data_loader-0.1.7/src/easy_data_loader.egg-info → easy_data_loader-0.1.8}/PKG-INFO +1 -1
  2. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/pyproject.toml +1 -1
  3. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/file_operations.py +21 -14
  4. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/pipeline.py +21 -9
  5. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8/src/easy_data_loader.egg-info}/PKG-INFO +1 -1
  6. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_file_operations.py +12 -7
  7. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_validation.py +60 -0
  8. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/LICENSE +0 -0
  9. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/README.md +0 -0
  10. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/setup.cfg +0 -0
  11. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/__init__.py +0 -0
  12. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/cli.py +0 -0
  13. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/config_loader.py +0 -0
  14. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/custom_exceptions.py +0 -0
  15. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/data_inferrence.py +0 -0
  16. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/database_connector.py +0 -0
  17. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/database_operations.py +0 -0
  18. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/driver_detector.py +0 -0
  19. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/log.py +0 -0
  20. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/models.py +0 -0
  21. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/orchestrator.py +0 -0
  22. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/pipeline_base.py +0 -0
  23. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/procedure_pipeline.py +0 -0
  24. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/resource_access.py +0 -0
  25. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader/utils.py +0 -0
  26. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/SOURCES.txt +0 -0
  27. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/dependency_links.txt +0 -0
  28. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/entry_points.txt +0 -0
  29. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/requires.txt +0 -0
  30. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/src/easy_data_loader.egg-info/top_level.txt +0 -0
  31. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_cli.py +0 -0
  32. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_config_loader.py +0 -0
  33. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_data_inference.py +0 -0
  34. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_database_connector.py +0 -0
  35. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_imports.py +0 -0
  36. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_models.py +0 -0
  37. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_orchestrator.py +0 -0
  38. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_pipeline_transform_order.py +0 -0
  39. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_procedure_execution_mode.py +0 -0
  40. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_resource_access.py +0 -0
  41. {easy_data_loader-0.1.7 → easy_data_loader-0.1.8}/tests/test_source_file_delete.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easy_data_loader
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "easy_data_loader"
3
- version = "0.1.7"
3
+ version = "0.1.8"
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,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
- output_path.parent.mkdir(parents=True, exist_ok=True)
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 output_path
96
+ return path
100
97
  except Exception as e:
101
- self.log_exception(e, f"Failed to write to: {output_path}")
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
- return base_path.with_suffix(file_extension)
116
- return (
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
- "orchestrator_id" : str(self.orchestrator_id),
295
- "pipeline_id" : str(self.pipeline_id),
296
- "pipeline_name" : self.definition.pipeline_name,
297
- "error": invalid_error_messages,
298
- "insert_timestamp" : current_timestamp})
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.write_file(df, **self.definition.write_parameters)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easy_data_loader
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -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.write_file(df)
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
- path = ops._construct_output_path()
77
- assert path == Path(tmpdir) / "fixed_output.csv"
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
- path = ops._construct_output_path()
87
- assert path.parent == Path(tmpdir)
88
- assert path.name.startswith("export_")
89
- assert path.suffix == ".csv"
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: