tablemaster 2.1.6__tar.gz → 2.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.
- {tablemaster-2.1.6 → tablemaster-2.1.8}/PKG-INFO +1 -1
- {tablemaster-2.1.6 → tablemaster-2.1.8}/pyproject.toml +1 -1
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/feishu.py +33 -15
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/diff.py +23 -2
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster.egg-info/PKG-INFO +1 -1
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tests/test_schema_core.py +37 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/LICENSE +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/README.md +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/setup.cfg +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/__init__.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/__main__.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/cli.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/config.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/database.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/gspread.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/local.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/__init__.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/apply.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/dialects/__init__.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/dialects/base.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/dialects/mysql.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/dialects/postgresql.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/dialects/tidb.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/init.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/introspect.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/loader.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/models.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/plan.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/schema/pull.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/sync.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster/utils.py +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster.egg-info/SOURCES.txt +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster.egg-info/dependency_links.txt +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster.egg-info/entry_points.txt +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster.egg-info/requires.txt +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tablemaster.egg-info/top_level.txt +0 -0
- {tablemaster-2.1.6 → tablemaster-2.1.8}/tests/test_error_visibility.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablemaster
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.8
|
|
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.
|
|
7
|
+
version = "2.1.8"
|
|
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"
|
|
@@ -226,15 +226,38 @@ def fs_write_df(sheet_address, df, feishu_cfg, loc='A1', clear_sheet=True):
|
|
|
226
226
|
|
|
227
227
|
# 清空工作表(如果需要)
|
|
228
228
|
if clear_sheet:
|
|
229
|
-
logger.info('clearing sheet
|
|
229
|
+
logger.info('clearing sheet before writing')
|
|
230
230
|
try:
|
|
231
|
-
clear_url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_clear"
|
|
232
231
|
clear_data = {"ranges": [f"{sheet_id}!A1:XFD1048576"]}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
clear_urls = [
|
|
233
|
+
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_clear",
|
|
234
|
+
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values/batch_clear",
|
|
235
|
+
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_clear/",
|
|
236
|
+
]
|
|
237
|
+
clear_ok = False
|
|
238
|
+
last_error = None
|
|
239
|
+
|
|
240
|
+
for clear_url in clear_urls:
|
|
241
|
+
clear_resp = _request_with_retry("post", clear_url, headers=header, json_data=clear_data)
|
|
242
|
+
if clear_resp.status_code == 404:
|
|
243
|
+
last_error = requests.HTTPError(
|
|
244
|
+
f'clear sheet endpoint not found: {clear_url}',
|
|
245
|
+
response=clear_resp
|
|
246
|
+
)
|
|
247
|
+
logger.warning('clear endpoint 404, try next: %s', clear_url)
|
|
248
|
+
continue
|
|
249
|
+
clear_body = _parse_json_response(clear_resp, f'clear sheet ({clear_url})')
|
|
250
|
+
_ensure_feishu_success(clear_body, f'clear sheet ({clear_url})')
|
|
251
|
+
clear_ok = True
|
|
252
|
+
logger.info('sheet cleared by endpoint: %s', clear_url)
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
if not clear_ok:
|
|
256
|
+
logger.warning(
|
|
257
|
+
'skip clear sheet because all clear endpoints returned 404; '
|
|
258
|
+
'writing will continue and old trailing cells may remain. last_error=%s',
|
|
259
|
+
last_error
|
|
260
|
+
)
|
|
238
261
|
except Exception as e:
|
|
239
262
|
logger.exception('failed to clear sheet: %s', e)
|
|
240
263
|
raise
|
|
@@ -296,14 +319,9 @@ def fs_write_df(sheet_address, df, feishu_cfg, loc='A1', clear_sheet=True):
|
|
|
296
319
|
|
|
297
320
|
try:
|
|
298
321
|
r = _request_with_retry("put", url, headers=header, json_data=post_data)
|
|
299
|
-
response = r
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
logger.info('data is written')
|
|
303
|
-
else:
|
|
304
|
-
logger.error('failed to write data: %s', response.get('msg', 'Unknown error'))
|
|
305
|
-
logger.error('error code: %s', response.get('code'))
|
|
306
|
-
|
|
322
|
+
response = _parse_json_response(r, 'write sheets values')
|
|
323
|
+
_ensure_feishu_success(response, 'write sheets values')
|
|
324
|
+
logger.info('data is written')
|
|
307
325
|
return response
|
|
308
326
|
|
|
309
327
|
except Exception as e:
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
|
|
3
5
|
from .dialects.base import BaseDialect
|
|
4
6
|
from .models import ActualTable, ColumnDef, Plan, PlanAction, TableDef
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
def
|
|
9
|
+
def _strip_outer_parens(value: str) -> str:
|
|
10
|
+
result = value.strip()
|
|
11
|
+
while result.startswith('(') and result.endswith(')'):
|
|
12
|
+
result = result[1:-1].strip()
|
|
13
|
+
return result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_pg_default(value: str) -> str:
|
|
17
|
+
normalized = _strip_outer_parens(value)
|
|
18
|
+
# PostgreSQL introspection often returns defaults like: 'N'::bpchar
|
|
19
|
+
# or ('unknown'::character varying). Strip trailing casts for comparison.
|
|
20
|
+
normalized = re.sub(r"::[a-zA-Z_][a-zA-Z0-9_\[\]\.\s]*$", '', normalized).strip()
|
|
21
|
+
if normalized.startswith("'") and normalized.endswith("'") and len(normalized) >= 2:
|
|
22
|
+
normalized = normalized[1:-1].replace("''", "'")
|
|
23
|
+
return normalized.upper()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _norm_default(value: str | None, dialect: BaseDialect) -> str | None:
|
|
8
27
|
if value is None:
|
|
9
28
|
return None
|
|
29
|
+
if dialect.__class__.__name__ == 'PostgreSQLDialect':
|
|
30
|
+
return _normalize_pg_default(value)
|
|
10
31
|
return value.strip().strip("'").upper()
|
|
11
32
|
|
|
12
33
|
|
|
@@ -161,7 +182,7 @@ def generate_plan(
|
|
|
161
182
|
)
|
|
162
183
|
)
|
|
163
184
|
|
|
164
|
-
if _norm_default(desired_col.default) != _norm_default(actual_col.default):
|
|
185
|
+
if _norm_default(desired_col.default, dialect) != _norm_default(actual_col.default, dialect):
|
|
165
186
|
plan.actions.append(
|
|
166
187
|
_action(
|
|
167
188
|
'ALTER_COLUMN_DEFAULT',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablemaster
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.8
|
|
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
|
|
@@ -244,6 +244,43 @@ class SchemaCoreTests(unittest.TestCase):
|
|
|
244
244
|
)
|
|
245
245
|
self.assertTrue(any(a.action == 'ADD_PRIMARY_KEY' for a in plan.actions))
|
|
246
246
|
|
|
247
|
+
def test_postgresql_default_literal_with_cast_not_repeated(self):
|
|
248
|
+
with TemporaryDirectory() as td:
|
|
249
|
+
root = Path(td)
|
|
250
|
+
schema_dir = root / 'schema' / 'mydb'
|
|
251
|
+
schema_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
(schema_dir / 'orders.yaml').write_text(
|
|
253
|
+
'\n'.join(
|
|
254
|
+
[
|
|
255
|
+
'table: orders',
|
|
256
|
+
'columns:',
|
|
257
|
+
' - name: archive',
|
|
258
|
+
' type: CHAR(1)',
|
|
259
|
+
' nullable: false',
|
|
260
|
+
' default: "\'N\'"',
|
|
261
|
+
]
|
|
262
|
+
),
|
|
263
|
+
encoding='utf-8',
|
|
264
|
+
)
|
|
265
|
+
desired = load_schema_definitions(connection='mydb', root_dir=root / 'schema')
|
|
266
|
+
actual = [
|
|
267
|
+
ActualTable(
|
|
268
|
+
table='orders',
|
|
269
|
+
columns=[
|
|
270
|
+
ActualColumn(
|
|
271
|
+
name='archive',
|
|
272
|
+
type='character(1)',
|
|
273
|
+
nullable=False,
|
|
274
|
+
default="'N'::bpchar",
|
|
275
|
+
comment=None,
|
|
276
|
+
)
|
|
277
|
+
],
|
|
278
|
+
indexes=[],
|
|
279
|
+
)
|
|
280
|
+
]
|
|
281
|
+
plan = generate_plan('mydb', desired, actual, PostgreSQLDialect())
|
|
282
|
+
self.assertFalse(any(a.action == 'ALTER_COLUMN_DEFAULT' for a in plan.actions))
|
|
283
|
+
|
|
247
284
|
|
|
248
285
|
if __name__ == '__main__':
|
|
249
286
|
unittest.main()
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|