etlplus 0.11.2__tar.gz → 0.11.9__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 (159) hide show
  1. {etlplus-0.11.2/etlplus.egg-info → etlplus-0.11.9}/PKG-INFO +1 -1
  2. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/handlers.py +1 -1
  3. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/database/ddl.py +1 -1
  4. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/database/engine.py +1 -1
  5. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/database/schema.py +1 -1
  6. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/__init__.py +0 -2
  7. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/core.py +50 -105
  8. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/enums.py +0 -28
  9. {etlplus-0.11.2 → etlplus-0.11.9/etlplus.egg-info}/PKG-INFO +1 -1
  10. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus.egg-info/SOURCES.txt +3 -1
  11. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_examples_data_parity.py +2 -2
  12. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/cli/test_u_cli_handlers.py +4 -9
  13. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/database/test_u_database_engine.py +5 -4
  14. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/database/test_u_database_schema.py +12 -14
  15. etlplus-0.11.2/tests/unit/test_u_file.py → etlplus-0.11.9/tests/unit/file/test_u_file_core.py +6 -154
  16. etlplus-0.11.9/tests/unit/file/test_u_file_enums.py +90 -0
  17. etlplus-0.11.9/tests/unit/file/test_u_file_yaml.py +110 -0
  18. {etlplus-0.11.2 → etlplus-0.11.9}/.coveragerc +0 -0
  19. {etlplus-0.11.2 → etlplus-0.11.9}/.editorconfig +0 -0
  20. {etlplus-0.11.2 → etlplus-0.11.9}/.gitattributes +0 -0
  21. {etlplus-0.11.2 → etlplus-0.11.9}/.github/actions/python-bootstrap/action.yml +0 -0
  22. {etlplus-0.11.2 → etlplus-0.11.9}/.github/workflows/ci.yml +0 -0
  23. {etlplus-0.11.2 → etlplus-0.11.9}/.gitignore +0 -0
  24. {etlplus-0.11.2 → etlplus-0.11.9}/.pre-commit-config.yaml +0 -0
  25. {etlplus-0.11.2 → etlplus-0.11.9}/.ruff.toml +0 -0
  26. {etlplus-0.11.2 → etlplus-0.11.9}/CODE_OF_CONDUCT.md +0 -0
  27. {etlplus-0.11.2 → etlplus-0.11.9}/CONTRIBUTING.md +0 -0
  28. {etlplus-0.11.2 → etlplus-0.11.9}/DEMO.md +0 -0
  29. {etlplus-0.11.2 → etlplus-0.11.9}/LICENSE +0 -0
  30. {etlplus-0.11.2 → etlplus-0.11.9}/MANIFEST.in +0 -0
  31. {etlplus-0.11.2 → etlplus-0.11.9}/Makefile +0 -0
  32. {etlplus-0.11.2 → etlplus-0.11.9}/README.md +0 -0
  33. {etlplus-0.11.2 → etlplus-0.11.9}/REFERENCES.md +0 -0
  34. {etlplus-0.11.2 → etlplus-0.11.9}/docs/README.md +0 -0
  35. {etlplus-0.11.2 → etlplus-0.11.9}/docs/pipeline-guide.md +0 -0
  36. {etlplus-0.11.2 → etlplus-0.11.9}/docs/snippets/installation_version.md +0 -0
  37. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/__init__.py +0 -0
  38. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/__main__.py +0 -0
  39. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/__version__.py +0 -0
  40. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/README.md +0 -0
  41. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/__init__.py +0 -0
  42. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/auth.py +0 -0
  43. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/config.py +0 -0
  44. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/endpoint_client.py +0 -0
  45. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/errors.py +0 -0
  46. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/pagination/__init__.py +0 -0
  47. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/pagination/client.py +0 -0
  48. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/pagination/config.py +0 -0
  49. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/pagination/paginator.py +0 -0
  50. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/rate_limiting/__init__.py +0 -0
  51. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/rate_limiting/config.py +0 -0
  52. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  53. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/request_manager.py +0 -0
  54. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/retry_manager.py +0 -0
  55. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/transport.py +0 -0
  56. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/api/types.py +0 -0
  57. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/__init__.py +0 -0
  58. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/commands.py +0 -0
  59. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/constants.py +0 -0
  60. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/io.py +0 -0
  61. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/main.py +0 -0
  62. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/options.py +0 -0
  63. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/state.py +0 -0
  64. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/cli/types.py +0 -0
  65. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/__init__.py +0 -0
  66. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/connector.py +0 -0
  67. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/jobs.py +0 -0
  68. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/pipeline.py +0 -0
  69. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/profile.py +0 -0
  70. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/types.py +0 -0
  71. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/config/utils.py +0 -0
  72. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/database/__init__.py +0 -0
  73. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/database/orm.py +0 -0
  74. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/database/types.py +0 -0
  75. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/enums.py +0 -0
  76. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/extract.py +0 -0
  77. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/csv.py +0 -0
  78. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/json.py +0 -0
  79. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/xml.py +0 -0
  80. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/file/yaml.py +0 -0
  81. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/load.py +0 -0
  82. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/mixins.py +0 -0
  83. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/py.typed +0 -0
  84. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/run.py +0 -0
  85. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/run_helpers.py +0 -0
  86. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/templates/__init__.py +0 -0
  87. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/templates/ddl.sql.j2 +0 -0
  88. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/templates/view.sql.j2 +0 -0
  89. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/transform.py +0 -0
  90. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/types.py +0 -0
  91. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/utils.py +0 -0
  92. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/validate.py +0 -0
  93. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/validation/__init__.py +0 -0
  94. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus/validation/utils.py +0 -0
  95. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus.egg-info/dependency_links.txt +0 -0
  96. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus.egg-info/entry_points.txt +0 -0
  97. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus.egg-info/requires.txt +0 -0
  98. {etlplus-0.11.2 → etlplus-0.11.9}/etlplus.egg-info/top_level.txt +0 -0
  99. {etlplus-0.11.2 → etlplus-0.11.9}/examples/README.md +0 -0
  100. {etlplus-0.11.2 → etlplus-0.11.9}/examples/configs/ddl_spec.yml +0 -0
  101. {etlplus-0.11.2 → etlplus-0.11.9}/examples/configs/pipeline.yml +0 -0
  102. {etlplus-0.11.2 → etlplus-0.11.9}/examples/data/sample.csv +0 -0
  103. {etlplus-0.11.2 → etlplus-0.11.9}/examples/data/sample.json +0 -0
  104. {etlplus-0.11.2 → etlplus-0.11.9}/examples/data/sample.xml +0 -0
  105. {etlplus-0.11.2 → etlplus-0.11.9}/examples/data/sample.xsd +0 -0
  106. {etlplus-0.11.2 → etlplus-0.11.9}/examples/data/sample.yaml +0 -0
  107. {etlplus-0.11.2 → etlplus-0.11.9}/examples/quickstart_python.py +0 -0
  108. {etlplus-0.11.2 → etlplus-0.11.9}/pyproject.toml +0 -0
  109. {etlplus-0.11.2 → etlplus-0.11.9}/pytest.ini +0 -0
  110. {etlplus-0.11.2 → etlplus-0.11.9}/setup.cfg +0 -0
  111. {etlplus-0.11.2 → etlplus-0.11.9}/setup.py +0 -0
  112. {etlplus-0.11.2 → etlplus-0.11.9}/tests/__init__.py +0 -0
  113. {etlplus-0.11.2 → etlplus-0.11.9}/tests/conftest.py +0 -0
  114. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/conftest.py +0 -0
  115. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_cli.py +0 -0
  116. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_pagination_strategy.py +0 -0
  117. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_pipeline_smoke.py +0 -0
  118. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  119. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_run.py +0 -0
  120. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  121. {etlplus-0.11.2 → etlplus-0.11.9}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  122. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/conftest.py +0 -0
  123. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_auth.py +0 -0
  124. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_config.py +0 -0
  125. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_endpoint_client.py +0 -0
  126. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_mocks.py +0 -0
  127. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_pagination_client.py +0 -0
  128. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_pagination_config.py +0 -0
  129. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_paginator.py +0 -0
  130. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  131. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_rate_limiter.py +0 -0
  132. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_request_manager.py +0 -0
  133. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_retry_manager.py +0 -0
  134. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_transport.py +0 -0
  135. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/api/test_u_types.py +0 -0
  136. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/cli/conftest.py +0 -0
  137. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/cli/test_u_cli_io.py +0 -0
  138. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/cli/test_u_cli_main.py +0 -0
  139. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/cli/test_u_cli_state.py +0 -0
  140. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/config/test_u_config_utils.py +0 -0
  141. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/config/test_u_connector.py +0 -0
  142. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/config/test_u_jobs.py +0 -0
  143. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/config/test_u_pipeline.py +0 -0
  144. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/conftest.py +0 -0
  145. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/database/test_u_database_ddl.py +0 -0
  146. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/database/test_u_database_orm.py +0 -0
  147. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_enums.py +0 -0
  148. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_extract.py +0 -0
  149. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_load.py +0 -0
  150. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_main.py +0 -0
  151. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_mixins.py +0 -0
  152. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_run.py +0 -0
  153. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_run_helpers.py +0 -0
  154. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_transform.py +0 -0
  155. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_utils.py +0 -0
  156. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_validate.py +0 -0
  157. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/test_u_version.py +0 -0
  158. {etlplus-0.11.2 → etlplus-0.11.9}/tests/unit/validation/test_u_validation_utils.py +0 -0
  159. {etlplus-0.11.2 → etlplus-0.11.9}/tools/update_demo_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.11.2
3
+ Version: 0.11.9
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -570,7 +570,7 @@ def transform_handler(
570
570
  data = transform(payload, cast(TransformOperations, operations_payload))
571
571
 
572
572
  if target and target != '-':
573
- File.write_file(target, data, file_format=target_format)
573
+ File(target, file_format=target_format).write(data)
574
574
  print(f'Data transformed and saved to {target}')
575
575
  return 0
576
576
 
@@ -203,7 +203,7 @@ def load_table_spec(
203
203
  raise ValueError('Spec must be .json, .yml, or .yaml')
204
204
 
205
205
  try:
206
- spec = File.read_file(spec_path)
206
+ spec = File(spec_path).read()
207
207
  except ImportError as e:
208
208
  if suffix in {'.yml', '.yaml'}:
209
209
  raise RuntimeError(
@@ -113,7 +113,7 @@ def load_database_url_from_config(
113
113
  ValueError
114
114
  If no connection string/URL/DSN is found for the specified entry.
115
115
  """
116
- cfg = File.read_file(Path(path))
116
+ cfg = File(Path(path)).read()
117
117
  if not isinstance(cfg, Mapping):
118
118
  raise TypeError('Database config must be a mapping')
119
119
 
@@ -260,7 +260,7 @@ def load_table_specs(
260
260
  list[TableSpec]
261
261
  A list of TableSpec instances parsed from the YAML file.
262
262
  """
263
- data = File.read_file(Path(path))
263
+ data = File(Path(path)).read()
264
264
  if not data:
265
265
  return []
266
266
 
@@ -9,7 +9,6 @@ from __future__ import annotations
9
9
  from .core import File
10
10
  from .enums import CompressionFormat
11
11
  from .enums import FileFormat
12
- from .enums import coerce_file_format
13
12
  from .enums import infer_file_format_and_compression
14
13
 
15
14
  # SECTION: EXPORTS ========================================================== #
@@ -22,6 +21,5 @@ __all__ = [
22
21
  'CompressionFormat',
23
22
  'FileFormat',
24
23
  # Functions
25
- 'coerce_file_format',
26
24
  'infer_file_format_and_compression',
27
25
  ]
@@ -11,7 +11,6 @@ from dataclasses import dataclass
11
11
  from pathlib import Path
12
12
 
13
13
  from ..types import JSONData
14
- from ..types import StrPath
15
14
  from . import csv
16
15
  from . import json
17
16
  from . import xml
@@ -43,7 +42,15 @@ class File:
43
42
  Path to the file on disk.
44
43
  file_format : FileFormat | None, optional
45
44
  Explicit format. If omitted, the format is inferred from the file
46
- extension (``.csv``, ``.json``, or ``.xml``).
45
+ extension (``.csv``, ``.json``, etc.).
46
+
47
+ Parameters
48
+ ----------
49
+ path : StrPath
50
+ Path to the file on disk.
51
+ file_format : FileFormat | str | None, optional
52
+ Explicit format. If omitted, the format is inferred from the file
53
+ extension (``.csv``, ``.json``, etc.).
47
54
  """
48
55
 
49
56
  # -- Attributes -- #
@@ -62,16 +69,10 @@ class File:
62
69
  extension is unknown, the attribute is left as ``None`` and will be
63
70
  validated later by :meth:`_ensure_format`.
64
71
  """
65
- # Normalize incoming path (allow str in constructor) to Path.
66
- if isinstance(self.path, str):
67
- self.path = Path(self.path)
68
-
72
+ self.path = Path(self.path)
73
+ self.file_format = self._coerce_format(self.file_format)
69
74
  if self.file_format is None:
70
- try:
71
- self.file_format = self._guess_format()
72
- except ValueError:
73
- # Leave as None; _ensure_format() will raise on use if needed.
74
- pass
75
+ self.file_format = self._maybe_guess_format()
75
76
 
76
77
  # -- Internal Instance Methods -- #
77
78
 
@@ -84,6 +85,28 @@ class File:
84
85
  if not self.path.exists():
85
86
  raise FileNotFoundError(f'File not found: {self.path}')
86
87
 
88
+ def _coerce_format(
89
+ self,
90
+ file_format: FileFormat | str | None,
91
+ ) -> FileFormat | None:
92
+ """
93
+ Normalize the file format input.
94
+
95
+ Parameters
96
+ ----------
97
+ file_format : FileFormat | str | None
98
+ File format specifier. Strings are coerced into
99
+ :class:`FileFormat`.
100
+
101
+ Returns
102
+ -------
103
+ FileFormat | None
104
+ A normalized file format, or ``None`` when unspecified.
105
+ """
106
+ if file_format is None or isinstance(file_format, FileFormat):
107
+ return file_format
108
+ return FileFormat.coerce(file_format)
109
+
87
110
  def _ensure_format(self) -> FileFormat:
88
111
  """
89
112
  Resolve the active format, guessing from extension if needed.
@@ -125,7 +148,22 @@ class File:
125
148
  f'Cannot infer file format from extension {self.path.suffix!r}',
126
149
  )
127
150
 
128
- # -- Instance Methods (Generic API) -- #
151
+ def _maybe_guess_format(self) -> FileFormat | None:
152
+ """
153
+ Try to infer the format, returning ``None`` if it cannot be inferred.
154
+
155
+ Returns
156
+ -------
157
+ FileFormat | None
158
+ The inferred format, or ``None`` if inference fails.
159
+ """
160
+ try:
161
+ return self._guess_format()
162
+ except ValueError:
163
+ # Leave as None; _ensure_format() will raise on use if needed.
164
+ return None
165
+
166
+ # -- Instance Methods -- #
129
167
 
130
168
  def read(self) -> JSONData:
131
169
  """
@@ -192,96 +230,3 @@ class File:
192
230
  case FileFormat.YAML:
193
231
  return yaml.write(self.path, data)
194
232
  raise ValueError(f'Unsupported format: {fmt}')
195
-
196
- # -- Class Methods -- #
197
-
198
- @classmethod
199
- def from_path(
200
- cls,
201
- path: StrPath,
202
- *,
203
- file_format: FileFormat | str | None = None,
204
- ) -> File:
205
- """
206
- Create a :class:`File` from any path-like and optional format.
207
-
208
- Parameters
209
- ----------
210
- path : StrPath
211
- Path to the file on disk.
212
- file_format : FileFormat | str | None, optional
213
- Explicit format. If omitted, the format is inferred from the file
214
- extension (``.csv``, ``.json``, or ``.xml``).
215
-
216
- Returns
217
- -------
218
- File
219
- The constructed :class:`File` instance.
220
- """
221
- resolved = Path(path)
222
- ff: FileFormat | None
223
- if isinstance(file_format, str):
224
- ff = FileFormat.coerce(file_format)
225
- else:
226
- ff = file_format
227
-
228
- return cls(resolved, ff)
229
-
230
- @classmethod
231
- def read_file(
232
- cls,
233
- path: StrPath,
234
- file_format: FileFormat | str | None = None,
235
- ) -> JSONData:
236
- """
237
- Read structured data.
238
-
239
- Parameters
240
- ----------
241
- path : StrPath
242
- Path to the file on disk.
243
- file_format : FileFormat | str | None, optional
244
- Explicit format. If omitted, the format is inferred from the file
245
- extension (``.csv``, ``.json``, or ``.xml``).
246
-
247
- Returns
248
- -------
249
- JSONData
250
- The structured data read from the file.
251
- """
252
- return cls.from_path(path, file_format=file_format).read()
253
-
254
- @classmethod
255
- def write_file(
256
- cls,
257
- path: StrPath,
258
- data: JSONData,
259
- file_format: FileFormat | str | None = None,
260
- *,
261
- root_tag: str = xml.DEFAULT_XML_ROOT,
262
- ) -> int:
263
- """
264
- Write structured data and count written records.
265
-
266
- Parameters
267
- ----------
268
- path : StrPath
269
- Path to the file on disk.
270
- data : JSONData
271
- Data to write to the file.
272
- file_format : FileFormat | str | None, optional
273
- Explicit format. If omitted, the format is inferred from the file
274
- extension (``.csv``, ``.json``, or ``.xml``).
275
- root_tag : str, optional
276
- Root tag name to use when writing XML files. Defaults to
277
- ``'root'``.
278
-
279
- Returns
280
- -------
281
- int
282
- The number of records written to the file.
283
- """
284
- return cls.from_path(path, file_format=file_format).write(
285
- data,
286
- root_tag=root_tag,
287
- )
@@ -16,8 +16,6 @@ from ..types import StrStrMap
16
16
  __all__ = [
17
17
  'CompressionFormat',
18
18
  'FileFormat',
19
- 'coerce_compression_format',
20
- 'coerce_file_format',
21
19
  'infer_file_format_and_compression',
22
20
  ]
23
21
 
@@ -164,32 +162,6 @@ _COMPRESSION_FILE_FORMATS: set[FileFormat] = {
164
162
  # SECTION: FUNCTIONS ======================================================== #
165
163
 
166
164
 
167
- # TODO: Deprecate in favor of using the enum methods directly.
168
- def coerce_compression_format(
169
- compression_format: CompressionFormat | str,
170
- ) -> CompressionFormat:
171
- """
172
- Normalize textual compression format values to :class:`CompressionFormat`.
173
-
174
- This thin wrapper is kept for backward compatibility; prefer
175
- :meth:`CompressionFormat.coerce` going forward.
176
- """
177
- return CompressionFormat.coerce(compression_format)
178
-
179
-
180
- # TODO: Deprecate in favor of using the enum methods directly.
181
- def coerce_file_format(
182
- file_format: FileFormat | str,
183
- ) -> FileFormat:
184
- """
185
- Normalize textual file format values to :class:`FileFormat`.
186
-
187
- This thin wrapper is kept for backward compatibility; prefer
188
- :meth:`FileFormat.coerce` going forward.
189
- """
190
- return FileFormat.coerce(file_format)
191
-
192
-
193
165
  # TODO: Convert to a method on FileFormat or CompressionFormat?
194
166
  def infer_file_format_and_compression(
195
167
  value: object,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.11.2
3
+ Version: 0.11.9
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -114,7 +114,6 @@ tests/integration/test_i_run_profile_rate_limit_defaults.py
114
114
  tests/unit/conftest.py
115
115
  tests/unit/test_u_enums.py
116
116
  tests/unit/test_u_extract.py
117
- tests/unit/test_u_file.py
118
117
  tests/unit/test_u_load.py
119
118
  tests/unit/test_u_main.py
120
119
  tests/unit/test_u_mixins.py
@@ -151,5 +150,8 @@ tests/unit/database/test_u_database_ddl.py
151
150
  tests/unit/database/test_u_database_engine.py
152
151
  tests/unit/database/test_u_database_orm.py
153
152
  tests/unit/database/test_u_database_schema.py
153
+ tests/unit/file/test_u_file_core.py
154
+ tests/unit/file/test_u_file_enums.py
155
+ tests/unit/file/test_u_file_yaml.py
154
156
  tests/unit/validation/test_u_validation_utils.py
155
157
  tools/update_demo_snippets.py
@@ -48,8 +48,8 @@ def test_examples_sample_csv_json_parity_integration():
48
48
  assert csv_path.exists(), f'Missing CSV fixture: {csv_path}'
49
49
  assert json_path.exists(), f'Missing JSON fixture: {json_path}'
50
50
 
51
- csv_data = File.read_file(csv_path)
52
- json_data = File.read_file(json_path)
51
+ csv_data = File(csv_path).read()
52
+ json_data = File(json_path).read()
53
53
 
54
54
  assert isinstance(csv_data, list), 'CSV should load as a list of dicts'
55
55
  assert isinstance(json_data, list), 'JSON should load as a list of dicts'
@@ -683,15 +683,11 @@ class TestTransformHandler:
683
683
  )
684
684
  write_calls: dict[str, object] = {}
685
685
 
686
- def fake_write(
687
- path: str,
688
- data: object,
689
- *,
690
- file_format: str | None,
691
- ) -> None:
692
- write_calls['params'] = (path, data, file_format)
686
+ def fake_write(self, data, **kwargs):
687
+ # Only capture path and data; ignore root_tag.
688
+ write_calls['params'] = (str(self.path), data)
693
689
 
694
- monkeypatch.setattr(handlers.File, 'write_file', fake_write)
690
+ monkeypatch.setattr(handlers.File, 'write', fake_write)
695
691
 
696
692
  assert (
697
693
  handlers.transform_handler(
@@ -709,7 +705,6 @@ class TestTransformHandler:
709
705
  'payload': {'source': 'data.json'},
710
706
  'ops': {'select': ['id']},
711
707
  },
712
- 'json',
713
708
  )
714
709
  assert (
715
710
  'Data transformed and saved to out.json' in capsys.readouterr().out
@@ -41,7 +41,8 @@ class TestLoadDatabaseUrlFromConfig:
41
41
  self,
42
42
  monkeypatch: pytest.MonkeyPatch,
43
43
  ) -> Callable[[Any], None]:
44
- """Return a helper that patches ``File.read_file`` with a payload.
44
+ """
45
+ Return a helper that patches :meth:`read` to return a payload.
45
46
 
46
47
  Parameters
47
48
  ----------
@@ -51,14 +52,14 @@ class TestLoadDatabaseUrlFromConfig:
51
52
  Returns
52
53
  -------
53
54
  Callable[[Any], None]
54
- Function that patches ``File.read_file`` to return the payload.
55
+ Function that patches ``File.read`` to return the payload.
55
56
  """
56
57
 
57
58
  def _apply(payload: Any) -> None:
58
59
  monkeypatch.setattr(
59
60
  engine_mod.File,
60
- 'read_file',
61
- staticmethod(lambda path: payload),
61
+ 'read',
62
+ lambda self: payload,
62
63
  )
63
64
 
64
65
  return _apply
@@ -76,7 +76,7 @@ class TestLoadTableSpecs:
76
76
 
77
77
  Notes
78
78
  -----
79
- Reuses a helper fixture to patch ``File.read_file`` and avoid disk IO.
79
+ Reuses a helper fixture to patch :meth:`File.read` and avoid disk IO.
80
80
  """
81
81
 
82
82
  @pytest.fixture()
@@ -84,7 +84,9 @@ class TestLoadTableSpecs:
84
84
  self,
85
85
  monkeypatch: pytest.MonkeyPatch,
86
86
  ) -> Callable[[Any], None]:
87
- """Return helper that patches ``File.read_file`` to return payload.
87
+ """
88
+ Return helper that patches the :meth:`read` instance method to return a
89
+ payload.
88
90
 
89
91
  Parameters
90
92
  ----------
@@ -98,18 +100,14 @@ class TestLoadTableSpecs:
98
100
  """
99
101
 
100
102
  def _apply(payload: Any) -> None:
101
- if callable(payload):
102
- monkeypatch.setattr(
103
- schema_mod.File,
104
- 'read_file',
105
- staticmethod(payload),
106
- )
107
- else:
108
- monkeypatch.setattr(
109
- schema_mod.File,
110
- 'read_file',
111
- staticmethod(lambda path: payload),
112
- )
103
+ # pylint: disable=unused-argument
104
+ """Apply the patch to :meth:`File.read` to return the payload."""
105
+
106
+ def fake_read(self, *args, **kwargs):
107
+ """Fake :meth:`File.read` method returning the payload."""
108
+ return payload(self.path) if callable(payload) else payload
109
+
110
+ monkeypatch.setattr(schema_mod.File, 'read', fake_read)
113
111
 
114
112
  return _apply
115
113
 
@@ -1,7 +1,7 @@
1
1
  """
2
- :mod:`tests.unit.test_u_file` module.
2
+ :mod:`tests.unit.test_u_file_core` module.
3
3
 
4
- Unit tests for :mod:`etlplus.file`.
4
+ Unit tests for :mod:`etlplus.file.core`.
5
5
 
6
6
  Notes
7
7
  -----
@@ -11,17 +11,13 @@ Notes
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
- from collections.abc import Generator
15
14
  from pathlib import Path
16
15
  from typing import cast
17
16
 
18
17
  import pytest
19
18
 
20
- import etlplus.file.yaml as yaml_module
21
- from etlplus.file import CompressionFormat
22
19
  from etlplus.file import File
23
20
  from etlplus.file import FileFormat
24
- from etlplus.file import infer_file_format_and_compression
25
21
  from etlplus.types import JSONDict
26
22
 
27
23
  # SECTION: HELPERS ========================================================== #
@@ -30,46 +26,6 @@ from etlplus.types import JSONDict
30
26
  pytestmark = pytest.mark.unit
31
27
 
32
28
 
33
- class _StubYaml:
34
- """Minimal PyYAML substitute to avoid optional dependency in tests."""
35
-
36
- def __init__(self) -> None:
37
- self.dump_calls: list[dict[str, object]] = []
38
-
39
- def safe_load(
40
- self,
41
- handle: object,
42
- ) -> dict[str, str]:
43
- """Stub for PyYAML's ``safe_load`` function."""
44
- text = ''
45
- if hasattr(handle, 'read'): # type: ignore[call-arg]
46
- text = handle.read()
47
- return {'loaded': str(text).strip()}
48
-
49
- def safe_dump(
50
- self,
51
- data: object,
52
- handle: object,
53
- **kwargs: object,
54
- ) -> None:
55
- """Stub for PyYAML's ``safe_dump`` function."""
56
- self.dump_calls.append({'data': data, 'kwargs': kwargs})
57
- if hasattr(handle, 'write'):
58
- handle.write('yaml') # type: ignore[call-arg]
59
-
60
-
61
- @pytest.fixture(name='yaml_stub')
62
- def yaml_stub_fixture() -> Generator[_StubYaml]:
63
- """Install a stub PyYAML module for YAML tests."""
64
- # pylint: disable=protected-access
65
-
66
- stub = _StubYaml()
67
- yaml_module._YAML_CACHE.clear()
68
- yaml_module._YAML_CACHE['mod'] = stub
69
- yield stub
70
- yaml_module._YAML_CACHE.clear()
71
-
72
-
73
29
  # SECTION: TESTS ============================================================ #
74
30
 
75
31
 
@@ -82,18 +38,18 @@ class TestFile:
82
38
  - Exercises JSON detection and defers errors for unknown extensions.
83
39
  """
84
40
 
85
- def test_classmethods_delegate(
41
+ def test_instance_methods_round_trip(
86
42
  self,
87
43
  tmp_path: Path,
88
44
  ) -> None:
89
45
  """
90
- Test that ``read_file`` and ``write_file`` round-trip via classmethods.
46
+ Test :meth:`read` and :meth:`write` round-tripping data.
91
47
  """
92
48
  path = tmp_path / 'delegated.json'
93
49
  data = {'name': 'delegated'}
94
50
 
95
- File.write_file(path, data, file_format='json')
96
- result = File.read_file(path, file_format='json')
51
+ File(path, file_format=FileFormat.JSON).write(data)
52
+ result = File(path, file_format=FileFormat.JSON).read()
97
53
 
98
54
  assert isinstance(result, dict)
99
55
  assert result['name'] == 'delegated'
@@ -303,107 +259,3 @@ class TestFile:
303
259
  text = path.read_text(encoding='utf-8')
304
260
  assert text.startswith('<?xml')
305
261
  assert '<records>' in text
306
-
307
-
308
- class TestFileFormat:
309
- """Unit test suite for :class:`etlplus.enums.FileFormat`."""
310
-
311
- @pytest.mark.parametrize(
312
- 'value,expected',
313
- [
314
- ('JSON', FileFormat.JSON),
315
- ('application/xml', FileFormat.XML),
316
- ('yml', FileFormat.YAML),
317
- ],
318
- )
319
- def test_aliases(
320
- self,
321
- value: str,
322
- expected: FileFormat,
323
- ) -> None:
324
- """Test alias coercions."""
325
- assert FileFormat.coerce(value) is expected
326
-
327
- def test_coerce(self) -> None:
328
- """Test :meth:`coerce`."""
329
- assert FileFormat.coerce('csv') is FileFormat.CSV
330
-
331
- def test_invalid_value(self) -> None:
332
- """Test that invalid values raise ValueError."""
333
- with pytest.raises(ValueError, match='Invalid FileFormat'):
334
- FileFormat.coerce('ini')
335
-
336
-
337
- class TestInferFileFormatAndCompression:
338
- """Unit test suite for :func:`infer_file_format_and_compression`."""
339
-
340
- @pytest.mark.parametrize(
341
- 'value,filename,expected_format,expected_compression',
342
- [
343
- ('data.csv.gz', None, FileFormat.CSV, CompressionFormat.GZ),
344
- ('data.jsonl.gz', None, FileFormat.NDJSON, CompressionFormat.GZ),
345
- ('data.zip', None, None, CompressionFormat.ZIP),
346
- ('application/json; charset=utf-8', None, FileFormat.JSON, None),
347
- ('application/gzip', None, None, CompressionFormat.GZ),
348
- (
349
- 'application/octet-stream',
350
- 'payload.csv.gz',
351
- FileFormat.CSV,
352
- CompressionFormat.GZ,
353
- ),
354
- ('application/octet-stream', None, None, None),
355
- (FileFormat.GZ, None, None, CompressionFormat.GZ),
356
- (CompressionFormat.ZIP, None, None, CompressionFormat.ZIP),
357
- ],
358
- )
359
- def test_infers_format_and_compression(
360
- self,
361
- value: object,
362
- filename: object | None,
363
- expected_format: FileFormat | None,
364
- expected_compression: CompressionFormat | None,
365
- ) -> None:
366
- """Test mixed inputs for format and compression inference."""
367
- fmt, compression = infer_file_format_and_compression(value, filename)
368
- assert fmt is expected_format
369
- assert compression is expected_compression
370
-
371
-
372
- @pytest.mark.unit
373
- class TestYamlSupport:
374
- """Unit tests exercising YAML read/write helpers without PyYAML."""
375
-
376
- def test_read_yaml_uses_stub(
377
- self,
378
- tmp_path: Path,
379
- yaml_stub: _StubYaml,
380
- ) -> None:
381
- """
382
- Test reading YAML should invoke stub ``safe_load``.
383
- """
384
- # pylint: disable=protected-access
385
-
386
- assert yaml_module._YAML_CACHE['mod'] is yaml_stub
387
- path = tmp_path / 'data.yaml'
388
- path.write_text('name: etl', encoding='utf-8')
389
-
390
- result = File(path, FileFormat.YAML).read()
391
-
392
- assert result == {'loaded': 'name: etl'}
393
-
394
- def test_write_yaml_uses_stub(
395
- self,
396
- tmp_path: Path,
397
- yaml_stub: _StubYaml,
398
- ) -> None:
399
- """
400
- Test writing YAML should invoke stub ``safe_dump``.
401
- """
402
- path = tmp_path / 'data.yaml'
403
- payload = [{'name': 'etl'}]
404
-
405
- written = File(path, FileFormat.YAML).write(payload)
406
-
407
- assert written == 1
408
- assert yaml_stub.dump_calls
409
- assert yaml_stub.dump_calls[0]['data'] == payload