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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: larksync
3
- Version: 0.1.0
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
- 3. 在应用的重定向 URL 中添加 `http://localhost:8888/callback`。
68
- 4. 发布应用版本。
69
-
102
+ ![权限导入](img/权限导入.png)
103
+ 3. 在应用的“安全设置”重定向 URL 中添加 `http://localhost:8888/callback`。
104
+ 4. 创建并发布应用版本。
105
+ ![重定向url](img/重定向URL.png)
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
- 3. 在应用的重定向 URL 中添加 `http://localhost:8888/callback`。
39
- 4. 发布应用版本。
40
-
73
+ ![权限导入](img/权限导入.png)
74
+ 3. 在应用的“安全设置”重定向 URL 中添加 `http://localhost:8888/callback`。
75
+ 4. 创建并发布应用版本。
76
+ ![重定向url](img/重定向URL.png)
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)。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "larksync"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "飞书电子表格 / 多维表格的拉取与按主键更新(Feishu / Lark sheets & bitable sync)"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- out = {}
88
- for i in range(0, len(refs), 100):
89
- data = self._post("/records/batch_get", {"record_ids": refs[i:i + 100]})
90
- for it in data.get("records") or []:
91
- out[it["record_id"]] = self._row(it.get("fields") or {})
92
- return out
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
- return self._table._apply(self)
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
- self._backend.update(cells)
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
- self._backend.update(cells)
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