tablemaster 2.1.5__tar.gz → 2.1.7__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.5 → tablemaster-2.1.7}/PKG-INFO +1 -1
- {tablemaster-2.1.5 → tablemaster-2.1.7}/pyproject.toml +1 -1
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/feishu.py +98 -27
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster.egg-info/PKG-INFO +1 -1
- {tablemaster-2.1.5 → tablemaster-2.1.7}/LICENSE +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/README.md +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/setup.cfg +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/__init__.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/__main__.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/cli.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/config.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/database.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/gspread.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/local.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/__init__.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/apply.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/dialects/__init__.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/dialects/base.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/dialects/mysql.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/dialects/postgresql.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/dialects/tidb.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/diff.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/init.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/introspect.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/loader.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/models.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/plan.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/schema/pull.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/sync.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster/utils.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster.egg-info/SOURCES.txt +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster.egg-info/dependency_links.txt +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster.egg-info/entry_points.txt +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster.egg-info/requires.txt +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tablemaster.egg-info/top_level.txt +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/tests/test_error_visibility.py +0 -0
- {tablemaster-2.1.5 → tablemaster-2.1.7}/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.
|
|
3
|
+
Version: 2.1.7
|
|
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.7"
|
|
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"
|
|
@@ -10,12 +10,53 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
_TOKEN_CACHE = {}
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _normalize_url(url):
|
|
14
|
+
normalized = url.strip()
|
|
15
|
+
if '`' in normalized:
|
|
16
|
+
logger.warning('url contains backticks, auto sanitize')
|
|
17
|
+
normalized = normalized.replace('`', '').strip()
|
|
18
|
+
return normalized
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_json_response(response, context):
|
|
22
|
+
if response.status_code >= 400:
|
|
23
|
+
body_preview = response.text[:500].replace('\n', ' ')
|
|
24
|
+
raise requests.HTTPError(
|
|
25
|
+
f'{context} failed with status={response.status_code}, body={body_preview}',
|
|
26
|
+
response=response
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
content_type = response.headers.get('Content-Type', '')
|
|
30
|
+
if 'application/json' not in content_type.lower():
|
|
31
|
+
body_preview = response.text[:500].replace('\n', ' ')
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f'{context} expected JSON but got Content-Type={content_type}, body={body_preview}'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
return response.json()
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
body_preview = response.text[:500].replace('\n', ' ')
|
|
40
|
+
raise ValueError(f'{context} invalid JSON response, body={body_preview}') from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ensure_feishu_success(body, context):
|
|
44
|
+
if not isinstance(body, dict):
|
|
45
|
+
raise ValueError(f'{context} expected dict JSON body')
|
|
46
|
+
code = body.get('code')
|
|
47
|
+
if code != 0:
|
|
48
|
+
msg = body.get('msg', 'Unknown error')
|
|
49
|
+
raise RuntimeError(f'{context} failed with code={code}, msg={msg}')
|
|
50
|
+
return body
|
|
51
|
+
|
|
52
|
+
|
|
13
53
|
def _request_with_retry(method, url, headers=None, params=None, json_data=None, data=None, timeout=30):
|
|
14
54
|
max_retries = 3
|
|
55
|
+
normalized_url = _normalize_url(url)
|
|
15
56
|
for attempt in range(max_retries):
|
|
16
57
|
response = requests.request(
|
|
17
58
|
method=method,
|
|
18
|
-
url=
|
|
59
|
+
url=normalized_url,
|
|
19
60
|
headers=headers,
|
|
20
61
|
params=params,
|
|
21
62
|
json=json_data,
|
|
@@ -45,8 +86,11 @@ def _get_tenant_access_token(feishu_cfg):
|
|
|
45
86
|
"app_secret": feishu_cfg.feishu_app_secret
|
|
46
87
|
}
|
|
47
88
|
r = _request_with_retry("post", feishu_url, json_data=post_data)
|
|
48
|
-
body = r
|
|
49
|
-
|
|
89
|
+
body = _parse_json_response(r, 'get tenant access token')
|
|
90
|
+
_ensure_feishu_success(body, 'get tenant access token')
|
|
91
|
+
token = body.get("tenant_access_token")
|
|
92
|
+
if not token:
|
|
93
|
+
raise KeyError('tenant_access_token is missing in auth response')
|
|
50
94
|
expire_seconds = int(body.get('expire', 7200))
|
|
51
95
|
_TOKEN_CACHE[cache_key] = {
|
|
52
96
|
'token': token,
|
|
@@ -91,7 +135,17 @@ def fs_read_df(sheet_address, feishu_cfg):
|
|
|
91
135
|
+ "?valueRenderOption=ToString&dateTimeRenderOption=FormattedString"
|
|
92
136
|
)
|
|
93
137
|
r = _request_with_retry("get", url, headers=header)
|
|
94
|
-
|
|
138
|
+
body = _parse_json_response(r, 'read sheets values')
|
|
139
|
+
_ensure_feishu_success(body, 'read sheets values')
|
|
140
|
+
|
|
141
|
+
pull_data = body.get('data', {}).get('valueRange', {}).get('values')
|
|
142
|
+
if not pull_data:
|
|
143
|
+
logger.info('sheet is empty: %s', sheet_address)
|
|
144
|
+
return pd.DataFrame()
|
|
145
|
+
if not isinstance(pull_data, list):
|
|
146
|
+
raise ValueError('unexpected sheets values structure: values is not a list')
|
|
147
|
+
if not pull_data[0]:
|
|
148
|
+
return pd.DataFrame(pull_data[1:] if len(pull_data) > 1 else [])
|
|
95
149
|
return pd.DataFrame(pull_data[1:], columns=pull_data[0])
|
|
96
150
|
|
|
97
151
|
|
|
@@ -127,7 +181,9 @@ def fs_read_base(sheet_address, feishu_cfg):
|
|
|
127
181
|
query_params += f"&page_token={page_token}"
|
|
128
182
|
|
|
129
183
|
r = _request_with_retry("get", base_url + query_params, headers=header)
|
|
130
|
-
|
|
184
|
+
body = _parse_json_response(r, 'read bitable records')
|
|
185
|
+
_ensure_feishu_success(body, 'read bitable records')
|
|
186
|
+
data = body.get('data', {})
|
|
131
187
|
pull_data.extend(data.get('items', []))
|
|
132
188
|
has_more = data.get('has_more', False)
|
|
133
189
|
page_token = data.get('page_token')
|
|
@@ -170,15 +226,38 @@ def fs_write_df(sheet_address, df, feishu_cfg, loc='A1', clear_sheet=True):
|
|
|
170
226
|
|
|
171
227
|
# 清空工作表(如果需要)
|
|
172
228
|
if clear_sheet:
|
|
173
|
-
logger.info('clearing sheet
|
|
229
|
+
logger.info('clearing sheet before writing')
|
|
174
230
|
try:
|
|
175
|
-
clear_url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_clear"
|
|
176
231
|
clear_data = {"ranges": [f"{sheet_id}!A1:XFD1048576"]}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
)
|
|
182
261
|
except Exception as e:
|
|
183
262
|
logger.exception('failed to clear sheet: %s', e)
|
|
184
263
|
raise
|
|
@@ -240,14 +319,9 @@ def fs_write_df(sheet_address, df, feishu_cfg, loc='A1', clear_sheet=True):
|
|
|
240
319
|
|
|
241
320
|
try:
|
|
242
321
|
r = _request_with_retry("put", url, headers=header, json_data=post_data)
|
|
243
|
-
response = r
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
logger.info('data is written')
|
|
247
|
-
else:
|
|
248
|
-
logger.error('failed to write data: %s', response.get('msg', 'Unknown error'))
|
|
249
|
-
logger.error('error code: %s', response.get('code'))
|
|
250
|
-
|
|
322
|
+
response = _parse_json_response(r, 'write sheets values')
|
|
323
|
+
_ensure_feishu_success(response, 'write sheets values')
|
|
324
|
+
logger.info('data is written')
|
|
251
325
|
return response
|
|
252
326
|
|
|
253
327
|
except Exception as e:
|
|
@@ -263,13 +337,10 @@ def _get_bitable_fields(app_token, table_id, header):
|
|
|
263
337
|
"""
|
|
264
338
|
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
|
|
265
339
|
r = _request_with_retry("get", url, headers=header)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
else:
|
|
271
|
-
logger.warning("failed to get fields: %s", r.json().get('msg', 'Unknown error'))
|
|
272
|
-
return set()
|
|
340
|
+
body = _parse_json_response(r, 'get bitable fields')
|
|
341
|
+
_ensure_feishu_success(body, 'get bitable fields')
|
|
342
|
+
items = body.get('data', {}).get('items', [])
|
|
343
|
+
return {item['field_name'] for item in items}
|
|
273
344
|
|
|
274
345
|
def fs_write_base(sheet_address, df, feishu_cfg, clear_table=False):
|
|
275
346
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablemaster
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.7
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|