pgmonkey 3.2.0__tar.gz → 3.3.0__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 (76) hide show
  1. {pgmonkey-3.2.0/src/pgmonkey.egg-info → pgmonkey-3.3.0}/PKG-INFO +1 -1
  2. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/pyproject.toml +1 -1
  3. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/pgexport_manager.py +1 -2
  4. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/pgimport_manager.py +1 -2
  5. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_csv_data_exporter.py +9 -0
  6. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_csv_data_importer.py +118 -0
  7. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tools/csv_data_exporter.py +1 -1
  8. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tools/csv_data_importer.py +9 -2
  9. {pgmonkey-3.2.0 → pgmonkey-3.3.0/src/pgmonkey.egg-info}/PKG-INFO +1 -1
  10. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/LICENSE +0 -0
  11. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/NOTICE +0 -0
  12. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/README.md +0 -0
  13. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/setup.cfg +0 -0
  14. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/__init__.py +0 -0
  15. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/__init__.py +0 -0
  16. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/__init__.py +0 -0
  17. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli.py +0 -0
  18. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli_export_subparser.py +0 -0
  19. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli_import_subparser.py +0 -0
  20. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli_pg_server_config_subparser.py +0 -0
  21. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli_pgconfig_subparser.py +0 -0
  22. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli_settings_subparser.py +0 -0
  23. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/cli/cli_toplevel_parser.py +0 -0
  24. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/__init__.py +0 -0
  25. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/config/__init__.py +0 -0
  26. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/exceptions.py +0 -0
  27. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/templates/postgres.yaml +0 -0
  28. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/utils/__init__.py +0 -0
  29. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/utils/configutils.py +0 -0
  30. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/common/utils/pathutils.py +0 -0
  31. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/__init__.py +0 -0
  32. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/base.py +0 -0
  33. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/__init__.py +0 -0
  34. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/async_connection.py +0 -0
  35. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/async_pool_connection.py +0 -0
  36. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/base_connection.py +0 -0
  37. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/normal_connection.py +0 -0
  38. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/pool_connection.py +0 -0
  39. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/connections/postgres/postgres_connection_factory.py +0 -0
  40. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/__init__.py +0 -0
  41. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/pg_server_config_manager.py +0 -0
  42. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/pgcodegen_manager.py +0 -0
  43. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/pgconfig_manager.py +0 -0
  44. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/pgconnection_manager.py +0 -0
  45. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/settings_manager.py +0 -0
  46. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/managers/toplevel_manager.py +0 -0
  47. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/serversettings/postgres_server_config_generator.py +0 -0
  48. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/serversettings/postgres_server_settings_inspector.py +0 -0
  49. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/settings/app_settings.yaml +0 -0
  50. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/__init__.py +0 -0
  51. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/conftest.py +0 -0
  52. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/integration/__init__.py +0 -0
  53. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/integration/test_pgconnection_manager_integration.py +0 -0
  54. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/__init__.py +0 -0
  55. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_async_connection.py +0 -0
  56. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_async_pool_connection.py +0 -0
  57. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_base_connection.py +0 -0
  58. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_code_generator.py +0 -0
  59. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_config_manager.py +0 -0
  60. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_connection_caching.py +0 -0
  61. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_connection_factory.py +0 -0
  62. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_normal_connection.py +0 -0
  63. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_path_utils.py +0 -0
  64. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_pgconnection_manager.py +0 -0
  65. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_pool_connection.py +0 -0
  66. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_server_config_generator.py +0 -0
  67. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_server_settings_inspector.py +0 -0
  68. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tests/unit/test_settings_manager.py +0 -0
  69. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tools/__init__.py +0 -0
  70. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tools/connection_code_generator.py +0 -0
  71. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey/tools/database_connection_tester.py +0 -0
  72. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey.egg-info/SOURCES.txt +0 -0
  73. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey.egg-info/dependency_links.txt +0 -0
  74. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey.egg-info/entry_points.txt +0 -0
  75. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey.egg-info/requires.txt +0 -0
  76. {pgmonkey-3.2.0 → pgmonkey-3.3.0}/src/pgmonkey.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgmonkey
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: A tool to assist with postgresql database connections
5
5
  Author-email: Good Boy <pythonic@rexbytes.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pgmonkey"
7
- version = "3.2.0"
7
+ version = "3.3.0"
8
8
  authors = [
9
9
  { name="Good Boy", email="pythonic@rexbytes.com" },
10
10
  ]
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  from pgmonkey.tools.csv_data_exporter import CSVDataExporter
3
2
  from pathlib import Path
4
3
 
@@ -22,5 +21,5 @@ class PGExportManager:
22
21
  exporter = CSVDataExporter(str(connection_config), table_name, str(csv_file), str(export_config_file))
23
22
 
24
23
  # Run the export process
25
- asyncio.run(exporter.run())
24
+ exporter.run()
26
25
 
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  from pgmonkey.tools.csv_data_importer import CSVDataImporter
3
2
  from pathlib import Path
4
3
 
@@ -19,4 +18,4 @@ class PGImportManager:
19
18
  importer = CSVDataImporter(str(connection_config), str(csv_file), table_name, str(import_config_file))
20
19
 
21
20
  # Run the import process
22
- asyncio.run(importer.run())
21
+ importer.run()
@@ -130,3 +130,12 @@ class TestResolveConnectionType:
130
130
 
131
131
  def test_async_pool_type_becomes_normal(self):
132
132
  assert CSVDataExporter._resolve_export_connection_type({'connection_type': 'async_pool'}) == 'normal'
133
+
134
+
135
+ class TestRunIsSync:
136
+ """run() should be a regular sync method, not async."""
137
+
138
+ def test_run_is_not_coroutine(self):
139
+ """run() should not be a coroutine function."""
140
+ import inspect
141
+ assert not inspect.iscoroutinefunction(CSVDataExporter.run)
@@ -191,3 +191,121 @@ class TestResolveConnectionType:
191
191
 
192
192
  def test_async_pool_type_becomes_normal(self):
193
193
  assert CSVDataImporter._resolve_import_connection_type({'connection_type': 'async_pool'}) == 'normal'
194
+
195
+
196
+ class _PastSamplingPhase(Exception):
197
+ """Sentinel exception to prove we got past phase 1 sampling."""
198
+
199
+
200
+ class TestSmallFileSampling:
201
+ """Phase 1 sampling must not crash on CSV files with fewer than 5 rows."""
202
+
203
+ def _make_importer(self, tmp_path, csv_content):
204
+ csv_file = tmp_path / "data.csv"
205
+ csv_file.write_text(csv_content)
206
+ config_file = tmp_path / "data.yaml"
207
+ config_file.write_text(yaml.dump({
208
+ 'has_headers': True, 'auto_create_table': True,
209
+ 'enforce_lowercase': True, 'delimiter': ',',
210
+ 'quotechar': '"', 'encoding': 'utf-8',
211
+ }))
212
+ return CSVDataImporter(
213
+ str(tmp_path / "conn.yaml"), str(csv_file),
214
+ "mytable", str(config_file),
215
+ )
216
+
217
+ @patch('pgmonkey.tools.csv_data_importer.CSVDataImporter._check_table_exists_sync',
218
+ side_effect=_PastSamplingPhase("reached table check"))
219
+ def test_single_row_csv_does_not_crash(self, mock_check, tmp_path):
220
+ """A CSV with only a header row (1 line) must not raise StopIteration."""
221
+ importer = self._make_importer(tmp_path, "col1,col2\n")
222
+ mock_conn = MagicMock()
223
+ mock_cursor = MagicMock()
224
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
225
+ mock_cursor.__exit__ = MagicMock(return_value=False)
226
+ mock_conn.cursor.return_value = mock_cursor
227
+ # If we get _PastSamplingPhase, sampling succeeded. StopIteration = bug.
228
+ with pytest.raises(_PastSamplingPhase):
229
+ importer._sync_ingest(mock_conn)
230
+
231
+ @patch('pgmonkey.tools.csv_data_importer.CSVDataImporter._check_table_exists_sync',
232
+ side_effect=_PastSamplingPhase("reached table check"))
233
+ def test_two_row_csv_does_not_crash(self, mock_check, tmp_path):
234
+ """A CSV with a header and one data row (2 lines) must not crash."""
235
+ importer = self._make_importer(tmp_path, "col1,col2\na,b\n")
236
+ mock_conn = MagicMock()
237
+ mock_cursor = MagicMock()
238
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
239
+ mock_cursor.__exit__ = MagicMock(return_value=False)
240
+ mock_conn.cursor.return_value = mock_cursor
241
+ with pytest.raises(_PastSamplingPhase):
242
+ importer._sync_ingest(mock_conn)
243
+
244
+ @patch('pgmonkey.tools.csv_data_importer.CSVDataImporter._check_table_exists_sync',
245
+ side_effect=_PastSamplingPhase("reached table check"))
246
+ def test_three_row_csv_does_not_crash(self, mock_check, tmp_path):
247
+ """A CSV with 3 lines must not crash in phase 1 sampling."""
248
+ importer = self._make_importer(tmp_path, "col1,col2\na,b\nc,d\n")
249
+ mock_conn = MagicMock()
250
+ mock_cursor = MagicMock()
251
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
252
+ mock_cursor.__exit__ = MagicMock(return_value=False)
253
+ mock_conn.cursor.return_value = mock_cursor
254
+ with pytest.raises(_PastSamplingPhase):
255
+ importer._sync_ingest(mock_conn)
256
+
257
+
258
+ class TestAutoCreateTable:
259
+ """auto_create_table=False should prevent table creation and raise ValueError."""
260
+
261
+ def _make_importer(self, tmp_path, auto_create=True):
262
+ csv_file = tmp_path / "data.csv"
263
+ csv_file.write_text("col1,col2\na,b\n")
264
+ config_file = tmp_path / "data.yaml"
265
+ config_file.write_text(yaml.dump({
266
+ 'has_headers': True, 'auto_create_table': auto_create,
267
+ 'enforce_lowercase': True, 'delimiter': ',',
268
+ 'quotechar': '"', 'encoding': 'utf-8',
269
+ }))
270
+ return CSVDataImporter(
271
+ str(tmp_path / "conn.yaml"), str(csv_file),
272
+ "mytable", str(config_file),
273
+ )
274
+
275
+ @patch('pgmonkey.tools.csv_data_importer.CSVDataImporter._check_table_exists_sync', return_value=False)
276
+ def test_auto_create_disabled_raises_when_table_missing(self, mock_check, tmp_path):
277
+ """When auto_create_table is False and table doesn't exist, should raise ValueError."""
278
+ importer = self._make_importer(tmp_path, auto_create=False)
279
+ mock_conn = MagicMock()
280
+ mock_cursor = MagicMock()
281
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
282
+ mock_cursor.__exit__ = MagicMock(return_value=False)
283
+ mock_conn.cursor.return_value = mock_cursor
284
+
285
+ with pytest.raises(ValueError, match="auto_create_table is disabled"):
286
+ importer._sync_ingest(mock_conn)
287
+
288
+ @patch('pgmonkey.tools.csv_data_importer.CSVDataImporter._create_table_sync',
289
+ side_effect=_PastSamplingPhase("table created"))
290
+ @patch('pgmonkey.tools.csv_data_importer.CSVDataImporter._check_table_exists_sync', return_value=False)
291
+ def test_auto_create_enabled_creates_table(self, mock_check, mock_create, tmp_path):
292
+ """When auto_create_table is True and table doesn't exist, should call _create_table_sync."""
293
+ importer = self._make_importer(tmp_path, auto_create=True)
294
+ mock_conn = MagicMock()
295
+ mock_cursor = MagicMock()
296
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
297
+ mock_cursor.__exit__ = MagicMock(return_value=False)
298
+ mock_conn.cursor.return_value = mock_cursor
299
+
300
+ with pytest.raises(_PastSamplingPhase):
301
+ importer._sync_ingest(mock_conn)
302
+ mock_create.assert_called_once()
303
+
304
+
305
+ class TestRunIsSync:
306
+ """run() should be a regular sync method, not async."""
307
+
308
+ def test_run_is_not_coroutine(self):
309
+ """run() should not be a coroutine function."""
310
+ import inspect
311
+ assert not inspect.iscoroutinefunction(CSVDataImporter.run)
@@ -179,7 +179,7 @@ class CSVDataExporter:
179
179
  # Restore the original client encoding
180
180
  self._restore_client_encoding(cur, original_encoding)
181
181
 
182
- async def run(self):
182
+ def run(self):
183
183
  """Main method to start the export using a normal sync connection for best COPY TO performance."""
184
184
  with open(self.config_file, 'r') as f:
185
185
  connection_config = yaml.safe_load(f)
@@ -249,7 +249,9 @@ class CSVDataImporter:
249
249
  with connection.cursor() as cur:
250
250
  # Phase 1: Detect column structure from a small sample
251
251
  with open(self.csv_file, 'r', encoding=self.encoding, newline='') as file:
252
- sample_rows = [next(file).strip() for _ in range(5)] # Read first 5 rows
252
+ sample_rows = []
253
+ for _, line in zip(range(5), file):
254
+ sample_rows.append(line.strip())
253
255
  sample_splits = [row.split(self.delimiter) for row in sample_rows]
254
256
  column_counts = [len(split) for split in sample_splits if any(split)] # Ignore empty lines
255
257
 
@@ -312,6 +314,11 @@ class CSVDataImporter:
312
314
  print(f"\nStarting import for file: {self.csv_file} into table: {self.schema_name}.{self.table_name}")
313
315
 
314
316
  if not self._check_table_exists_sync(connection):
317
+ if not self.auto_create_table:
318
+ raise ValueError(
319
+ f"Table {self.schema_name}.{self.table_name} does not exist and "
320
+ "auto_create_table is disabled in the import configuration."
321
+ )
315
322
  self._create_table_sync(connection, formatted_headers)
316
323
  print(f"\nTable {self.schema_name}.{self.table_name} created successfully.")
317
324
  else:
@@ -389,7 +396,7 @@ class CSVDataImporter:
389
396
  )
390
397
  return cur.fetchone()[0]
391
398
 
392
- async def run(self):
399
+ def run(self):
393
400
  """Main method to start the ingestion using a normal sync connection for best COPY performance."""
394
401
  with open(self.config_file, 'r') as f:
395
402
  connection_config = yaml.safe_load(f)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgmonkey
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: A tool to assist with postgresql database connections
5
5
  Author-email: Good Boy <pythonic@rexbytes.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes