tablemaster 2.1.1__tar.gz → 2.1.2__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 (37) hide show
  1. {tablemaster-2.1.1 → tablemaster-2.1.2}/PKG-INFO +1 -1
  2. {tablemaster-2.1.1 → tablemaster-2.1.2}/pyproject.toml +1 -1
  3. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/database.py +13 -8
  4. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/feishu.py +14 -9
  5. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/gspread.py +11 -7
  6. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster.egg-info/PKG-INFO +1 -1
  7. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster.egg-info/SOURCES.txt +1 -0
  8. tablemaster-2.1.2/tests/test_error_visibility.py +54 -0
  9. {tablemaster-2.1.1 → tablemaster-2.1.2}/LICENSE +0 -0
  10. {tablemaster-2.1.1 → tablemaster-2.1.2}/README.md +0 -0
  11. {tablemaster-2.1.1 → tablemaster-2.1.2}/setup.cfg +0 -0
  12. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/__init__.py +0 -0
  13. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/__main__.py +0 -0
  14. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/cli.py +0 -0
  15. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/config.py +0 -0
  16. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/local.py +0 -0
  17. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/__init__.py +0 -0
  18. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/apply.py +0 -0
  19. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/dialects/__init__.py +0 -0
  20. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/dialects/base.py +0 -0
  21. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/dialects/mysql.py +0 -0
  22. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/dialects/postgresql.py +0 -0
  23. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/dialects/tidb.py +0 -0
  24. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/diff.py +0 -0
  25. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/init.py +0 -0
  26. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/introspect.py +0 -0
  27. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/loader.py +0 -0
  28. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/models.py +0 -0
  29. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/plan.py +0 -0
  30. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/schema/pull.py +0 -0
  31. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/sync.py +0 -0
  32. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster/utils.py +0 -0
  33. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster.egg-info/dependency_links.txt +0 -0
  34. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster.egg-info/entry_points.txt +0 -0
  35. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster.egg-info/requires.txt +0 -0
  36. {tablemaster-2.1.1 → tablemaster-2.1.2}/tablemaster.egg-info/top_level.txt +0 -0
  37. {tablemaster-2.1.1 → tablemaster-2.1.2}/tests/test_schema_core.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tablemaster
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: tablemaster is a Python toolkit for moving and managing tabular data across databases, Feishu/Lark, Google Sheets, and local files with one consistent API.
5
5
  Author-email: Livid <livid.su@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/ilivid/tablemaster
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tablemaster"
7
- version = "2.1.1"
7
+ version = "2.1.2"
8
8
  description = "tablemaster is a Python toolkit for moving and managing tabular data across databases, Feishu/Lark, Google Sheets, and local files with one consistent API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -5,7 +5,7 @@ import warnings
5
5
  from typing import Union, List, Tuple, Dict, Any, Optional
6
6
  from functools import lru_cache
7
7
 
8
- from sqlalchemy import create_engine, pool, text
8
+ from sqlalchemy import create_engine, inspect, pool, text
9
9
  from sqlalchemy.engine import Engine
10
10
  import pandas as pd
11
11
  from datetime import datetime
@@ -230,12 +230,13 @@ class ManageTable:
230
230
  bool: True if table exists, False otherwise.
231
231
  """
232
232
  safe_table: str = _safe_identifier(self.table)
233
- check_sql = text(f'SELECT 1 FROM `{safe_table}` LIMIT 1')
234
233
  try:
235
- opt(check_sql, self)
236
- return True
237
- except Exception:
238
- return False
234
+ engine: Engine = _resolve_engine(self.configs if hasattr(self, 'configs') else self, autocommit=False)
235
+ inspector = inspect(engine)
236
+ return inspector.has_table(safe_table)
237
+ except Exception as e:
238
+ logger.exception('failed to check if table exists: %s', e)
239
+ raise
239
240
 
240
241
  def delete_table(self) -> None:
241
242
  """
@@ -245,8 +246,9 @@ class ManageTable:
245
246
  try:
246
247
  opt(text(f'DROP TABLE `{safe_table}`'), self)
247
248
  logger.info('%s deleted', self.table)
248
- except Exception:
249
- logger.exception('table was not deleted')
249
+ except Exception as e:
250
+ logger.exception('table was not deleted: %s', e)
251
+ raise
250
252
 
251
253
  def par_del(self, clause: str, params: Optional[Dict[str, Any]] = None) -> None:
252
254
  """
@@ -305,6 +307,7 @@ class ManageTable:
305
307
  pbar.update(1)
306
308
  except Exception as e:
307
309
  logger.exception('an error occurred during upload: %s', e)
310
+ raise
308
311
 
309
312
  def upsert_data(self, df: pd.DataFrame, chunk_size: int = 10000, add_date: bool = False, ignore: bool = False, key: Union[str, List[str], Tuple[str, ...], None] = None) -> None:
310
313
  """
@@ -411,6 +414,7 @@ class ManageTable:
411
414
  pbar.update(1)
412
415
  except Exception as e:
413
416
  logger.exception('an error occurred during upsert: %s', e)
417
+ raise
414
418
 
415
419
  class Manage_table(ManageTable):
416
420
  """
@@ -466,3 +470,4 @@ class Manage_table(ManageTable):
466
470
  pbar.update(1)
467
471
  except Exception as e:
468
472
  logger.exception('an error occurred during upload: %s', e)
473
+ raise
@@ -178,9 +178,10 @@ def fs_write_df(sheet_address, df, feishu_cfg, loc='A1', clear_sheet=True):
178
178
  if clear_resp.json().get('code') == 0:
179
179
  logger.info('sheet cleared')
180
180
  else:
181
- logger.warning("failed to clear sheet: %s", clear_resp.json().get('msg'))
181
+ raise RuntimeError(f"failed to clear sheet: {clear_resp.json().get('msg')}")
182
182
  except Exception as e:
183
- logger.warning('failed to clear sheet: %s', e)
183
+ logger.exception('failed to clear sheet: %s', e)
184
+ raise
184
185
 
185
186
  # 处理 DataFrame 数据类型
186
187
  df_copy = df.copy()
@@ -305,8 +306,7 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
305
306
  existing_fields = _get_bitable_fields(app_token, table_id, header)
306
307
 
307
308
  if not existing_fields:
308
- logger.error('could not fetch table fields or table has no fields')
309
- return None
309
+ raise ValueError('could not fetch table fields or table has no fields')
310
310
 
311
311
  logger.info('table has %s fields', len(existing_fields))
312
312
 
@@ -323,8 +323,7 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
323
323
  logger.warning('skip column: %s', field)
324
324
 
325
325
  if not valid_fields:
326
- logger.error('no valid fields to write, all dataframe columns are missing in bitable')
327
- return None
326
+ raise ValueError('no valid fields to write, all dataframe columns are missing in bitable')
328
327
 
329
328
  logger.info('will write %s valid fields', len(valid_fields))
330
329
 
@@ -360,8 +359,9 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
360
359
  _request_with_retry("post", delete_url, headers=header, json_data=delete_data)
361
360
  logger.info('deleted %s records', len(record_ids))
362
361
 
363
- except Exception as e:
364
- logger.warning('failed to clear table: %s', e)
362
+ except Exception as e:
363
+ logger.exception('failed to clear table: %s', e)
364
+ raise
365
365
 
366
366
  # 处理 DataFrame - 只保留有效字段
367
367
  df_copy = df[list(valid_fields)].copy()
@@ -444,7 +444,7 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
444
444
  str_val = str(value)
445
445
  if str_val and str_val != 'None' and str_val != 'nan':
446
446
  fields[col] = str_val
447
- except:
447
+ except Exception:
448
448
  if col not in skipped_cols:
449
449
  skipped_cols.add(col)
450
450
  continue
@@ -457,6 +457,7 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
457
457
  # 批量写入(每次最多500条)
458
458
  batch_size = 500
459
459
  all_responses = []
460
+ failed_batches = []
460
461
 
461
462
  for i in range(0, len(records), batch_size):
462
463
  batch = records[i:i + batch_size]
@@ -473,9 +474,11 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
473
474
  logger.info('batch %s wrote %s records', i // batch_size + 1, len(batch))
474
475
  else:
475
476
  logger.error('failed to write batch: %s', response.get('msg', 'Unknown error'))
477
+ failed_batches.append((i // batch_size + 1, response.get('msg', 'Unknown error')))
476
478
 
477
479
  except Exception as e:
478
480
  logger.exception('failed to write batch: %s', e)
481
+ failed_batches.append((i // batch_size + 1, str(e)))
479
482
 
480
483
  logger.info('write summary total records: %s', len(records))
481
484
  logger.info('write summary fields written: %s', len(valid_fields))
@@ -483,6 +486,8 @@ def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
483
486
  logger.info('write summary fields skipped: %s', len(missing_fields))
484
487
  for field in sorted(missing_fields):
485
488
  logger.info('skip field: %s', field)
489
+ if failed_batches:
490
+ raise RuntimeError(f'bitable write failed for {len(failed_batches)} batch(es): {failed_batches}')
486
491
  logger.info('data is written')
487
492
 
488
493
  return all_responses
@@ -68,14 +68,16 @@ def gs_read_df(address, cfg=None, service_account_path=None):
68
68
  return df
69
69
 
70
70
  except gspread.exceptions.SpreadsheetNotFound:
71
- logger.error("spreadsheet '%s' not found", spreadsheet_identifier)
72
- return None
71
+ message = f"spreadsheet '{spreadsheet_identifier}' not found"
72
+ logger.error(message)
73
+ raise ValueError(message)
73
74
  except gspread.exceptions.WorksheetNotFound:
74
- logger.error("worksheet '%s' not found in spreadsheet", worksheet_name)
75
- return None
75
+ message = f"worksheet '{worksheet_name}' not found in spreadsheet"
76
+ logger.error(message)
77
+ raise ValueError(message)
76
78
  except Exception as e:
77
79
  logger.exception('an unexpected error occurred: %s', e)
78
- return None
80
+ raise
79
81
 
80
82
 
81
83
  def gs_write_df(address, df, cfg=None, loc='A1', service_account_path=None):
@@ -105,8 +107,9 @@ def gs_write_df(address, df, cfg=None, loc='A1', service_account_path=None):
105
107
 
106
108
  except gspread.exceptions.SpreadsheetNotFound:
107
109
  if is_id:
108
- logger.error("spreadsheet ID '%s' not found, cannot create with specific ID", spreadsheet_identifier)
109
- return
110
+ message = f"spreadsheet ID '{spreadsheet_identifier}' not found, cannot create with specific ID"
111
+ logger.error(message)
112
+ raise ValueError(message)
110
113
  else:
111
114
  logger.info("spreadsheet '%s' not found, creating one", spreadsheet_identifier)
112
115
  sh = gc.create(spreadsheet_identifier)
@@ -128,3 +131,4 @@ def gs_write_df(address, df, cfg=None, loc='A1', service_account_path=None):
128
131
  logger.info('data is written')
129
132
  except Exception as e:
130
133
  logger.exception('failed to update worksheet: %s', e)
134
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tablemaster
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: tablemaster is a Python toolkit for moving and managing tabular data across databases, Feishu/Lark, Google Sheets, and local files with one consistent API.
5
5
  Author-email: Livid <livid.su@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/ilivid/tablemaster
@@ -31,4 +31,5 @@ tablemaster/schema/dialects/base.py
31
31
  tablemaster/schema/dialects/mysql.py
32
32
  tablemaster/schema/dialects/postgresql.py
33
33
  tablemaster/schema/dialects/tidb.py
34
+ tests/test_error_visibility.py
34
35
  tests/test_schema_core.py
@@ -0,0 +1,54 @@
1
+ from types import SimpleNamespace
2
+ from unittest import TestCase
3
+ from unittest.mock import patch
4
+
5
+ import pandas as pd
6
+
7
+ from tablemaster.database import ManageTable
8
+ from tablemaster.feishu import fs_write_base
9
+
10
+
11
+ class _DummyResponse:
12
+ def __init__(self, body, status_code=200):
13
+ self._body = body
14
+ self.status_code = status_code
15
+
16
+ def json(self):
17
+ return self._body
18
+
19
+
20
+ class ErrorVisibilityTests(TestCase):
21
+ def setUp(self):
22
+ self.db_cfg = SimpleNamespace(
23
+ name='test_db',
24
+ user='u',
25
+ password='p',
26
+ host='127.0.0.1',
27
+ database='d',
28
+ db_type='mysql',
29
+ )
30
+ self.feishu_cfg = SimpleNamespace(feishu_app_id='id', feishu_app_secret='secret')
31
+
32
+ def test_manage_table_exists_propagates_errors(self):
33
+ table = ManageTable('orders', self.db_cfg)
34
+ with patch('tablemaster.database._resolve_engine', side_effect=RuntimeError('db unavailable')):
35
+ with self.assertRaises(RuntimeError):
36
+ table.exists()
37
+
38
+ def test_delete_table_propagates_errors(self):
39
+ table = ManageTable('orders', self.db_cfg)
40
+ with patch('tablemaster.database.opt', side_effect=RuntimeError('drop failed')):
41
+ with self.assertRaises(RuntimeError):
42
+ table.delete_table()
43
+
44
+ def test_fs_write_base_raises_when_batch_write_failed(self):
45
+ df = pd.DataFrame({'a': [1]})
46
+
47
+ with patch('tablemaster.feishu._get_tenant_access_token', return_value='token'):
48
+ with patch('tablemaster.feishu._get_bitable_fields', return_value={'a'}):
49
+ with patch(
50
+ 'tablemaster.feishu._request_with_retry',
51
+ return_value=_DummyResponse({'code': 1001, 'msg': 'bad request'}),
52
+ ):
53
+ with self.assertRaises(RuntimeError):
54
+ fs_write_base(['app_token', 'table_id'], df, self.feishu_cfg)
File without changes
File without changes
File without changes