larksync 0.1.0__tar.gz → 0.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.
- {larksync-0.1.0 → larksync-0.1.2}/PKG-INFO +48 -9
- {larksync-0.1.0 → larksync-0.1.2}/README.md +47 -8
- {larksync-0.1.0 → larksync-0.1.2}/pyproject.toml +1 -1
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/bitable.py +9 -7
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/diff.py +6 -3
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/table.py +28 -7
- {larksync-0.1.0 → larksync-0.1.2}/tests/test_flow.py +11 -0
- {larksync-0.1.0 → larksync-0.1.2}/.gitignore +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/LICENSE +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/docs/python.md +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/__init__.py +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/auth.py +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/history.py +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/http.py +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/src/larksync/sheets.py +0 -0
- {larksync-0.1.0 → larksync-0.1.2}/tests/test_offline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: larksync
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: 飞书电子表格 / 多维表格的拉取与按主键更新(Feishu / Lark sheets & bitable sync)
|
|
5
5
|
Project-URL: Homepage, https://github.com/libcyz/larksync
|
|
6
6
|
Project-URL: Documentation, https://github.com/libcyz/larksync/blob/main/docs/python.md
|
|
@@ -29,9 +29,9 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
# larksync
|
|
31
31
|
|
|
32
|
-
这是一个用来同步飞书表格的项目,大多数项目的非技术人员在使用飞书在线表格进行管理的时候经常会出现大量数据需要人工核对,可能又需要去重,于是添加新的表变的非常麻烦,而整理好的csv中如果出现重复的数据,直接复制的话就gameover
|
|
32
|
+
这是一个用来同步飞书表格的项目,大多数项目的非技术人员在使用飞书在线表格进行管理的时候经常会出现大量数据需要人工核对,可能又需要去重,于是添加新的表变的非常麻烦,而整理好的csv中如果出现重复的数据,直接复制的话就gameover,当然会有用豆包更新表格的声音,如果你使用豆包更新表格,那我不要和你说话。
|
|
33
33
|
|
|
34
|
-
对于技术人员来讲,在pipeline搭建好之后发现跑脚本前需要拉取在线表格更新后的信息,需要折返到对应表格,下载,另存为csv,大概率还会出现XXXX表(1),XXXX表(2),然后就需要在修改脚本路径和重命名表格这两个愚蠢选项中选择一项,诸如此类,larksync
|
|
34
|
+
对于技术人员来讲,在pipeline搭建好之后发现跑脚本前需要拉取在线表格更新后的信息,需要折返到对应表格,下载,另存为csv,大概率还会出现XXXX表(1),XXXX表(2),然后就需要在修改脚本路径和重命名表格这两个愚蠢选项中选择一项,诸如此类,larksync是为了方便复用诞生的。
|
|
35
35
|
|
|
36
36
|
## 更新规则
|
|
37
37
|
|
|
@@ -42,31 +42,67 @@ GUI 和 Python 代码使用同一套规则。
|
|
|
42
42
|
- 新增的行由调用方决定是否写入。GUI 默认不勾选新增的行。
|
|
43
43
|
- 本地文件中为空的格子不参与比较。larksync 不清空在线表的值。
|
|
44
44
|
- larksync 不删除在线表的行。撤销时,只删除本次写入新增的行。
|
|
45
|
-
- 写入前,larksync
|
|
45
|
+
- 写入前,larksync 重新读取需要的行。预览后有人修改过这些格子时,写入就终止。
|
|
46
46
|
|
|
47
47
|
## 前提
|
|
48
48
|
|
|
49
|
-
1.
|
|
50
|
-
2
|
|
49
|
+
1. 创建一个飞书自建应用(企业自建应用):https://open.feishu.cn/?lang=zh-CN
|
|
50
|
+
2.“权限管理”中为应用开通以下权限:
|
|
51
51
|
```json
|
|
52
52
|
{
|
|
53
53
|
"scopes": {
|
|
54
54
|
"tenant": [
|
|
55
|
+
"aily:knowledge:read",
|
|
56
|
+
"base:record:retrieve",
|
|
55
57
|
"bitable:app",
|
|
58
|
+
"bitable:app:readonly",
|
|
59
|
+
"docs:document.media:download",
|
|
60
|
+
"drive:drive.metadata:readonly",
|
|
61
|
+
"drive:drive:version",
|
|
62
|
+
"drive:drive:version:readonly",
|
|
63
|
+
"im:message",
|
|
64
|
+
"im:message.group_at_msg.include_bot:readonly",
|
|
65
|
+
"im:message.group_at_msg:readonly",
|
|
66
|
+
"im:message.p2p_msg:readonly",
|
|
67
|
+
"im:message.pins:read",
|
|
68
|
+
"im:message.reactions:read",
|
|
69
|
+
"im:message:readonly",
|
|
70
|
+
"im:message:recall",
|
|
71
|
+
"im:message:send_as_bot",
|
|
72
|
+
"im:message:update",
|
|
73
|
+
"im:resource",
|
|
56
74
|
"sheets:spreadsheet",
|
|
75
|
+
"sheets:spreadsheet.meta:read",
|
|
76
|
+
"sheets:spreadsheet.meta:write_only",
|
|
77
|
+
"sheets:spreadsheet:create",
|
|
78
|
+
"sheets:spreadsheet:read",
|
|
79
|
+
"sheets:spreadsheet:readonly",
|
|
80
|
+
"sheets:spreadsheet:write_only",
|
|
81
|
+
"wiki:wiki",
|
|
57
82
|
"wiki:wiki:readonly"
|
|
58
83
|
],
|
|
59
84
|
"user": [
|
|
85
|
+
"aily:knowledge:read",
|
|
86
|
+
"base:record:retrieve",
|
|
60
87
|
"bitable:app",
|
|
88
|
+
"bitable:app:readonly",
|
|
61
89
|
"sheets:spreadsheet",
|
|
90
|
+
"sheets:spreadsheet.meta:read",
|
|
91
|
+
"sheets:spreadsheet.meta:write_only",
|
|
92
|
+
"sheets:spreadsheet:create",
|
|
93
|
+
"sheets:spreadsheet:read",
|
|
94
|
+
"sheets:spreadsheet:readonly",
|
|
95
|
+
"sheets:spreadsheet:write_only",
|
|
96
|
+
"wiki:wiki",
|
|
62
97
|
"wiki:wiki:readonly"
|
|
63
98
|
]
|
|
64
99
|
}
|
|
65
100
|
}
|
|
66
101
|
```
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+

|
|
103
|
+
3. 在应用的“安全设置”重定向 URL 中添加 `http://localhost:8888/callback`。
|
|
104
|
+
4. 创建并发布应用版本。
|
|
105
|
+

|
|
70
106
|
说明:权限修改并发布后,生效可以有几分钟的延迟;每个人必须使用自己的飞书应用,不同的人对在线表的权限不同。
|
|
71
107
|
|
|
72
108
|
## GUI 使用
|
|
@@ -109,4 +145,7 @@ GUI 和 Python 代码使用同一套规则。
|
|
|
109
145
|
- 单击「移除」:删除本机的表格配置。飞书上的在线表和写入记录不变。
|
|
110
146
|
- 单击右上角的用户名:修改 App ID 或 App Secret,或退出登录。
|
|
111
147
|
|
|
148
|
+
### 注意
|
|
149
|
+
如果你的windows开着防火墙,windows可能会把这个组件标记成不安全,选择“属性”--“解除锁定” 再重新解压即可。
|
|
150
|
+
|
|
112
151
|
开发者调用详见 [docs/python.md](https://github.com/libcyz/larksync/blob/main/docs/python.md)。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# larksync
|
|
2
2
|
|
|
3
|
-
这是一个用来同步飞书表格的项目,大多数项目的非技术人员在使用飞书在线表格进行管理的时候经常会出现大量数据需要人工核对,可能又需要去重,于是添加新的表变的非常麻烦,而整理好的csv中如果出现重复的数据,直接复制的话就gameover
|
|
3
|
+
这是一个用来同步飞书表格的项目,大多数项目的非技术人员在使用飞书在线表格进行管理的时候经常会出现大量数据需要人工核对,可能又需要去重,于是添加新的表变的非常麻烦,而整理好的csv中如果出现重复的数据,直接复制的话就gameover,当然会有用豆包更新表格的声音,如果你使用豆包更新表格,那我不要和你说话。
|
|
4
4
|
|
|
5
|
-
对于技术人员来讲,在pipeline搭建好之后发现跑脚本前需要拉取在线表格更新后的信息,需要折返到对应表格,下载,另存为csv,大概率还会出现XXXX表(1),XXXX表(2),然后就需要在修改脚本路径和重命名表格这两个愚蠢选项中选择一项,诸如此类,larksync
|
|
5
|
+
对于技术人员来讲,在pipeline搭建好之后发现跑脚本前需要拉取在线表格更新后的信息,需要折返到对应表格,下载,另存为csv,大概率还会出现XXXX表(1),XXXX表(2),然后就需要在修改脚本路径和重命名表格这两个愚蠢选项中选择一项,诸如此类,larksync是为了方便复用诞生的。
|
|
6
6
|
|
|
7
7
|
## 更新规则
|
|
8
8
|
|
|
@@ -13,31 +13,67 @@ GUI 和 Python 代码使用同一套规则。
|
|
|
13
13
|
- 新增的行由调用方决定是否写入。GUI 默认不勾选新增的行。
|
|
14
14
|
- 本地文件中为空的格子不参与比较。larksync 不清空在线表的值。
|
|
15
15
|
- larksync 不删除在线表的行。撤销时,只删除本次写入新增的行。
|
|
16
|
-
- 写入前,larksync
|
|
16
|
+
- 写入前,larksync 重新读取需要的行。预览后有人修改过这些格子时,写入就终止。
|
|
17
17
|
|
|
18
18
|
## 前提
|
|
19
19
|
|
|
20
|
-
1.
|
|
21
|
-
2
|
|
20
|
+
1. 创建一个飞书自建应用(企业自建应用):https://open.feishu.cn/?lang=zh-CN
|
|
21
|
+
2.“权限管理”中为应用开通以下权限:
|
|
22
22
|
```json
|
|
23
23
|
{
|
|
24
24
|
"scopes": {
|
|
25
25
|
"tenant": [
|
|
26
|
+
"aily:knowledge:read",
|
|
27
|
+
"base:record:retrieve",
|
|
26
28
|
"bitable:app",
|
|
29
|
+
"bitable:app:readonly",
|
|
30
|
+
"docs:document.media:download",
|
|
31
|
+
"drive:drive.metadata:readonly",
|
|
32
|
+
"drive:drive:version",
|
|
33
|
+
"drive:drive:version:readonly",
|
|
34
|
+
"im:message",
|
|
35
|
+
"im:message.group_at_msg.include_bot:readonly",
|
|
36
|
+
"im:message.group_at_msg:readonly",
|
|
37
|
+
"im:message.p2p_msg:readonly",
|
|
38
|
+
"im:message.pins:read",
|
|
39
|
+
"im:message.reactions:read",
|
|
40
|
+
"im:message:readonly",
|
|
41
|
+
"im:message:recall",
|
|
42
|
+
"im:message:send_as_bot",
|
|
43
|
+
"im:message:update",
|
|
44
|
+
"im:resource",
|
|
27
45
|
"sheets:spreadsheet",
|
|
46
|
+
"sheets:spreadsheet.meta:read",
|
|
47
|
+
"sheets:spreadsheet.meta:write_only",
|
|
48
|
+
"sheets:spreadsheet:create",
|
|
49
|
+
"sheets:spreadsheet:read",
|
|
50
|
+
"sheets:spreadsheet:readonly",
|
|
51
|
+
"sheets:spreadsheet:write_only",
|
|
52
|
+
"wiki:wiki",
|
|
28
53
|
"wiki:wiki:readonly"
|
|
29
54
|
],
|
|
30
55
|
"user": [
|
|
56
|
+
"aily:knowledge:read",
|
|
57
|
+
"base:record:retrieve",
|
|
31
58
|
"bitable:app",
|
|
59
|
+
"bitable:app:readonly",
|
|
32
60
|
"sheets:spreadsheet",
|
|
61
|
+
"sheets:spreadsheet.meta:read",
|
|
62
|
+
"sheets:spreadsheet.meta:write_only",
|
|
63
|
+
"sheets:spreadsheet:create",
|
|
64
|
+
"sheets:spreadsheet:read",
|
|
65
|
+
"sheets:spreadsheet:readonly",
|
|
66
|
+
"sheets:spreadsheet:write_only",
|
|
67
|
+
"wiki:wiki",
|
|
33
68
|
"wiki:wiki:readonly"
|
|
34
69
|
]
|
|
35
70
|
}
|
|
36
71
|
}
|
|
37
72
|
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+

|
|
74
|
+
3. 在应用的“安全设置”重定向 URL 中添加 `http://localhost:8888/callback`。
|
|
75
|
+
4. 创建并发布应用版本。
|
|
76
|
+

|
|
41
77
|
说明:权限修改并发布后,生效可以有几分钟的延迟;每个人必须使用自己的飞书应用,不同的人对在线表的权限不同。
|
|
42
78
|
|
|
43
79
|
## GUI 使用
|
|
@@ -80,4 +116,7 @@ GUI 和 Python 代码使用同一套规则。
|
|
|
80
116
|
- 单击「移除」:删除本机的表格配置。飞书上的在线表和写入记录不变。
|
|
81
117
|
- 单击右上角的用户名:修改 App ID 或 App Secret,或退出登录。
|
|
82
118
|
|
|
119
|
+
### 注意
|
|
120
|
+
如果你的windows开着防火墙,windows可能会把这个组件标记成不安全,选择“属性”--“解除锁定” 再重新解压即可。
|
|
121
|
+
|
|
83
122
|
开发者调用详见 [docs/python.md](https://github.com/libcyz/larksync/blob/main/docs/python.md)。
|
|
@@ -82,14 +82,16 @@ class Bitable:
|
|
|
82
82
|
params={"page_size": 1}, json={})["data"].get("total", 0)
|
|
83
83
|
|
|
84
84
|
def fetch(self, refs):
|
|
85
|
-
"""只读指定的记录:{record_id: 记录}
|
|
85
|
+
"""只读指定的记录:{record_id: 记录},已删除的不在结果里。每批 100 条、并发 5 个。"""
|
|
86
86
|
self.columns()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
|
|
88
|
+
def get(chunk):
|
|
89
|
+
return self._post("/records/batch_get", {"record_ids": chunk}).get("records") or []
|
|
90
|
+
|
|
91
|
+
chunks = [refs[i:i + 100] for i in range(0, len(refs), 100)]
|
|
92
|
+
with ThreadPoolExecutor(_WORKERS) as pool:
|
|
93
|
+
return {it["record_id"]: self._row(it.get("fields") or {})
|
|
94
|
+
for part in pool.map(get, chunks) for it in part}
|
|
93
95
|
|
|
94
96
|
def writable(self, name):
|
|
95
97
|
return self._fields.get(name) in _WRITABLE
|
|
@@ -36,9 +36,12 @@ class Plan:
|
|
|
36
36
|
return (f"补空 {len(self.changed)} 格 / 新增 {len(self.added)} 行 / "
|
|
37
37
|
f"不一致 {len(self.conflicts)} 格(未写入)/ 未匹配 {self.unmatched} 行")
|
|
38
38
|
|
|
39
|
-
def apply(self):
|
|
40
|
-
"""写入飞书,返回写入记录 id(用于 Table.undo)。
|
|
41
|
-
|
|
39
|
+
def apply(self, progress=None):
|
|
40
|
+
"""写入飞书,返回写入记录 id(用于 Table.undo)。
|
|
41
|
+
|
|
42
|
+
progress(阶段, 已完成, 总数):可选的进度回调,阶段是「检查」或「写入」。
|
|
43
|
+
"""
|
|
44
|
+
return self._table._apply(self, progress)
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
def load(source):
|
|
@@ -11,6 +11,7 @@ from .http import LarkError, call
|
|
|
11
11
|
from .sheets import Sheets
|
|
12
12
|
|
|
13
13
|
_DENIED = {91403, 131006, 1254302}
|
|
14
|
+
_PROGRESS_STEP = 1000 # 每写这么多格回调一次进度
|
|
14
15
|
_MISSING_SCOPE = {99991672, 99991679}
|
|
15
16
|
|
|
16
17
|
|
|
@@ -104,14 +105,18 @@ class Table:
|
|
|
104
105
|
self._write(rec, [], added)
|
|
105
106
|
return rec["id"]
|
|
106
107
|
|
|
107
|
-
def undo(self, record_id):
|
|
108
|
-
"""撤销一次写入:还原改过的格子、删掉新增的行。之后又被别人改过的话整体中止。
|
|
108
|
+
def undo(self, record_id, progress=None):
|
|
109
|
+
"""撤销一次写入:还原改过的格子、删掉新增的行。之后又被别人改过的话整体中止。
|
|
110
|
+
|
|
111
|
+
progress(阶段, 已完成, 总数):可选的进度回调,阶段是「检查」或「撤销」。
|
|
112
|
+
"""
|
|
109
113
|
rec = history.load(record_id)
|
|
110
114
|
if rec["target"] != self._backend.target:
|
|
111
115
|
raise LarkError(f"写入记录 {record_id} 不属于这张表(它属于 {rec['url']})")
|
|
112
116
|
if rec["status"] == "undone":
|
|
113
117
|
raise LarkError(f"写入记录 {record_id} 已经撤销过了")
|
|
114
118
|
with self._explained():
|
|
119
|
+
_report(progress, "检查", 0, len(rec["changed"]) + len(rec["created"]))
|
|
115
120
|
current = self._backend.fetch([ch["ref"] for ch in rec["changed"]] + rec["created"])
|
|
116
121
|
conflicts, cells, drop = [], [], []
|
|
117
122
|
for ch in rec["changed"]:
|
|
@@ -131,13 +136,18 @@ class Table:
|
|
|
131
136
|
drop.append(ref)
|
|
132
137
|
if conflicts:
|
|
133
138
|
raise LarkError("撤销会覆盖别人之后的修改,已中止:\n" + brief(conflicts))
|
|
134
|
-
|
|
139
|
+
total = len(cells) + len(drop)
|
|
140
|
+
for i in range(0, len(cells), _PROGRESS_STEP):
|
|
141
|
+
self._backend.update(cells[i:i + _PROGRESS_STEP])
|
|
142
|
+
_report(progress, "撤销", min(i + _PROGRESS_STEP, len(cells)), total)
|
|
135
143
|
self._backend.delete(drop)
|
|
144
|
+
_report(progress, "撤销", total, total)
|
|
136
145
|
rec["status"] = "undone"
|
|
137
146
|
history.save(rec)
|
|
138
147
|
|
|
139
|
-
def _apply(self, plan):
|
|
148
|
+
def _apply(self, plan, progress=None):
|
|
140
149
|
with self._explained():
|
|
150
|
+
_report(progress, "检查", 0, len(plan.changed) + len(plan.added))
|
|
141
151
|
current = self._backend.fetch([ch.ref for ch in plan.changed]) # 只读涉及的行
|
|
142
152
|
taken = {}
|
|
143
153
|
if plan.added: # 确认新增的主键没被别人抢先加上
|
|
@@ -158,14 +168,20 @@ class Table:
|
|
|
158
168
|
raise LarkError("在线表在 diff 之后被改过,已中止写入,请重新 diff:\n"
|
|
159
169
|
+ brief(conflicts))
|
|
160
170
|
rec = history.new(self.url, self._backend.target, plan.key, plan.changed, plan.added)
|
|
161
|
-
self._write(rec, cells, plan.added)
|
|
171
|
+
self._write(rec, cells, plan.added, progress)
|
|
162
172
|
return rec["id"]
|
|
163
173
|
|
|
164
|
-
def _write(self, rec, cells, added):
|
|
165
|
-
|
|
174
|
+
def _write(self, rec, cells, added, progress=None):
|
|
175
|
+
total = len(cells) + len(added)
|
|
176
|
+
for i in range(0, len(cells), _PROGRESS_STEP):
|
|
177
|
+
self._backend.update(cells[i:i + _PROGRESS_STEP])
|
|
178
|
+
_report(progress, "写入", min(i + _PROGRESS_STEP, len(cells)), total)
|
|
166
179
|
for refs in self._backend.create(added):
|
|
180
|
+
if not refs:
|
|
181
|
+
continue
|
|
167
182
|
rec["created"] += refs
|
|
168
183
|
history.save(rec) # 每批落盘,中途失败也能撤销已写入的部分
|
|
184
|
+
_report(progress, "写入", len(cells) + len(rec["created"]), total)
|
|
169
185
|
rec["status"] = "done"
|
|
170
186
|
history.save(rec)
|
|
171
187
|
|
|
@@ -199,6 +215,11 @@ class Table:
|
|
|
199
215
|
raise
|
|
200
216
|
|
|
201
217
|
|
|
218
|
+
def _report(progress, stage, done, total):
|
|
219
|
+
if progress:
|
|
220
|
+
progress(stage, done, total)
|
|
221
|
+
|
|
222
|
+
|
|
202
223
|
def _cell(row, key_cols, key, column):
|
|
203
224
|
"""行还在、主键没变时返回该格的值,否则 None(行被删或电子表格行位置变了)。"""
|
|
204
225
|
if row is None or tuple(row.get(c, "") for c in key_cols) != tuple(key):
|
|
@@ -153,3 +153,14 @@ def test_diff_validation():
|
|
|
153
153
|
t.diff([{"ID": "1", "状态": "b"}, {"ID": "1", "状态": "c"}], key="ID")
|
|
154
154
|
with pytest.raises(LarkError, match="主键为空"):
|
|
155
155
|
t.diff([{"ID": "", "状态": "b"}], key="ID")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_progress_callback():
|
|
159
|
+
t = table(["ID", "状态"], [{"ID": str(i), "状态": ""} for i in range(2500)])
|
|
160
|
+
plan = t.diff([{"ID": str(i), "状态": "x"} for i in range(2500)], key="ID")
|
|
161
|
+
seen = []
|
|
162
|
+
rid = plan.apply(progress=lambda stage, done, total: seen.append((stage, done, total)))
|
|
163
|
+
assert seen == [("检查", 0, 2500), ("写入", 1000, 2500), ("写入", 2000, 2500), ("写入", 2500, 2500)]
|
|
164
|
+
seen.clear()
|
|
165
|
+
t.undo(rid, progress=lambda *a: seen.append(a))
|
|
166
|
+
assert seen[0] == ("检查", 0, 2500) and seen[-1] == ("撤销", 2500, 2500)
|
|
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
|