p115client 0.0.5.14.2__tar.gz → 0.0.5.14.3__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.
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/PKG-INFO +2 -2
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/iterdir.py +290 -11
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/pyproject.toml +2 -2
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/LICENSE +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/__init__.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/_upload.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/client.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/const.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/exception.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/py.typed +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/__init__.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/attr.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/auth.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/download.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/edit.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/export_dir.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/fs_files.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/history.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/life.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/offline.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/pool.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/request.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/upload.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/util.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/tool/xys.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/p115client/type.py +0 -0
- {p115client-0.0.5.14.2 → p115client-0.0.5.14.3}/readme.md +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: p115client
|
3
|
-
Version: 0.0.5.14.
|
3
|
+
Version: 0.0.5.14.3
|
4
4
|
Summary: Python 115 webdisk client.
|
5
5
|
Home-page: https://github.com/ChenyangGao/p115client
|
6
6
|
License: MIT
|
@@ -41,7 +41,7 @@ Requires-Dist: python-filewrap (>=0.2.8)
|
|
41
41
|
Requires-Dist: python-hashtools (>=0.0.3.3)
|
42
42
|
Requires-Dist: python-http_request (>=0.0.6)
|
43
43
|
Requires-Dist: python-httpfile (>=0.0.5.2)
|
44
|
-
Requires-Dist: python-iterutils (>=0.2.5.
|
44
|
+
Requires-Dist: python-iterutils (>=0.2.5.4)
|
45
45
|
Requires-Dist: python-property (>=0.0.3)
|
46
46
|
Requires-Dist: python-startfile (>=0.0.2)
|
47
47
|
Requires-Dist: python-undefined (>=0.0.3)
|
@@ -11,27 +11,29 @@ __all__ = [
|
|
11
11
|
"ensure_attr_path", "ensure_attr_path_using_star_event",
|
12
12
|
"iterdir", "iter_stared_dirs", "iter_dirs", "iter_dirs_with_path",
|
13
13
|
"iter_files", "iter_files_with_path", "iter_files_with_path_skim",
|
14
|
-
"
|
15
|
-
"
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"
|
14
|
+
"traverse_tree", "traverse_tree_with_path", "iter_nodes",
|
15
|
+
"iter_nodes_skim", "iter_nodes_by_pickcode", "iter_nodes_using_update",
|
16
|
+
"iter_nodes_using_info", "iter_nodes_using_star_event",
|
17
|
+
"iter_dir_nodes_using_star", "iter_parents", "iter_files_shortcut",
|
18
|
+
"iter_dupfiles", "iter_image_files", "search_iter", "share_iterdir",
|
19
|
+
"share_iter_files", "share_search_iter",
|
19
20
|
]
|
20
21
|
__doc__ = "这个模块提供了一些和目录信息罗列有关的函数"
|
21
22
|
|
22
|
-
# TODO: 再实现 2 个方法,利用 iter_download_nodes,一个有 path,一个没有,可以把某个目录下的所有节点都搞出来,导出时,先导出目录节点,再导出文件节点,但它们是并发执行的,然后必有字段:id, parent_id, pickcode, name, is_dir, sha1 等
|
23
23
|
# TODO: 路径表示法,应该支持 / 和 > 开头,而不仅仅是 / 开头
|
24
24
|
# TODO: get_id* 这类方法,应该放在 attr.py,用来获取某个 id 对应的值(根本还是 get_attr)
|
25
25
|
# TODO: 创造函数 get_id, get_parent_id, get_ancestors, get_sha1, get_pickcode, get_path 等,支持多种类型的参数,目前已有的名字太长,需要改造,甚至转为私有,另外这些函数或许可以放到另一个包中,attr.py
|
26
26
|
# TODO: 去除掉一些并不便利的办法,然后加上 traverse 和 walk 方法,通过递归拉取(支持深度和广度优先遍历)
|
27
27
|
# TODO: 要获取某个 id 对应的路径,可以先用 fs_file_skim 或 fs_info 看一下是不是存在,以及是不是文件,然后再选择响应最快的办法获取
|
28
28
|
|
29
|
-
from asyncio import create_task, sleep as async_sleep
|
29
|
+
from asyncio import create_task, sleep as async_sleep, Task
|
30
30
|
from collections import defaultdict
|
31
31
|
from collections.abc import (
|
32
|
-
AsyncIterable, AsyncIterator, Callable, Coroutine,
|
33
|
-
Iterator, Mapping, MutableMapping, Sequence,
|
32
|
+
AsyncIterable, AsyncIterator, Callable, Coroutine, Generator,
|
33
|
+
Iterable, Iterator, Mapping, MutableMapping, Sequence,
|
34
34
|
)
|
35
|
+
from contextlib import contextmanager
|
36
|
+
from concurrent.futures import Future
|
35
37
|
from dataclasses import dataclass
|
36
38
|
from errno import EIO, ENOENT, ENOTDIR
|
37
39
|
from functools import partial
|
@@ -183,6 +185,76 @@ def _update_resp_ancestors(
|
|
183
185
|
return resp
|
184
186
|
|
185
187
|
|
188
|
+
def _make_top_adder(
|
189
|
+
top_id: int,
|
190
|
+
id_to_dirnode: MutableMapping[int, tuple[str, int] | DirNode],
|
191
|
+
) -> Callable:
|
192
|
+
top_ancestors: list[dict]
|
193
|
+
if not top_id:
|
194
|
+
top_ancestors = [{"id": 0, "parent_id": 0, "name": ""}]
|
195
|
+
def add_top[T: MutableMapping](attr: T, /) -> T:
|
196
|
+
nonlocal top_ancestors
|
197
|
+
try:
|
198
|
+
top_ancestors
|
199
|
+
except NameError:
|
200
|
+
top_ancestors = []
|
201
|
+
add_ancestor = top_ancestors.append
|
202
|
+
tid = top_id
|
203
|
+
while tid and tid in id_to_dirnode:
|
204
|
+
name, pid = id_to_dirnode[tid]
|
205
|
+
add_ancestor({"id": tid, "parent_id": pid, "name": name})
|
206
|
+
tid = pid
|
207
|
+
if not tid:
|
208
|
+
add_ancestor({"id": 0, "parent_id": 0, "name": ""})
|
209
|
+
top_ancestors.reverse()
|
210
|
+
attr["top_id"] = top_id
|
211
|
+
attr["top_ancestors"] = top_ancestors
|
212
|
+
return attr
|
213
|
+
return add_top
|
214
|
+
|
215
|
+
|
216
|
+
@overload
|
217
|
+
@contextmanager
|
218
|
+
def cache_loading[T](
|
219
|
+
it: Iterator[T],
|
220
|
+
/,
|
221
|
+
) -> Generator[tuple[list[T], Future]]:
|
222
|
+
...
|
223
|
+
@overload
|
224
|
+
@contextmanager
|
225
|
+
def cache_loading[T](
|
226
|
+
it: AsyncIterator[T],
|
227
|
+
/,
|
228
|
+
) -> Generator[tuple[list[T], Task]]:
|
229
|
+
...
|
230
|
+
@contextmanager
|
231
|
+
def cache_loading[T](
|
232
|
+
it: Iterator[T] | AsyncIterator[T],
|
233
|
+
/,
|
234
|
+
) -> Generator[tuple[list[T], Future | Task]]:
|
235
|
+
cache: list[T] = []
|
236
|
+
add_to_cache = cache.append
|
237
|
+
running = True
|
238
|
+
if isinstance(it, AsyncIterator):
|
239
|
+
async def arunner():
|
240
|
+
async for e in it:
|
241
|
+
add_to_cache(e)
|
242
|
+
if not running:
|
243
|
+
break
|
244
|
+
task: Future | Task = create_task(arunner())
|
245
|
+
else:
|
246
|
+
def runner():
|
247
|
+
for e in it:
|
248
|
+
add_to_cache(e)
|
249
|
+
if not running:
|
250
|
+
break
|
251
|
+
task = run_as_thread(runner)
|
252
|
+
try:
|
253
|
+
yield (cache, task)
|
254
|
+
finally:
|
255
|
+
running = False
|
256
|
+
|
257
|
+
|
186
258
|
# TODO: 支持 open
|
187
259
|
@overload
|
188
260
|
def get_path_to_cid(
|
@@ -2037,7 +2109,8 @@ def iter_dirs_with_path(
|
|
2037
2109
|
async_=async_, # type: ignore
|
2038
2110
|
**request_kwargs,
|
2039
2111
|
))
|
2040
|
-
|
2112
|
+
add_top = _make_top_adder(to_id(cid), id_to_dirnode)
|
2113
|
+
return YieldFrom(do_map(add_top, ensure_attr_path(
|
2041
2114
|
client,
|
2042
2115
|
attrs,
|
2043
2116
|
with_ancestors=with_ancestors,
|
@@ -2046,7 +2119,7 @@ def iter_dirs_with_path(
|
|
2046
2119
|
app=app,
|
2047
2120
|
async_=async_,
|
2048
2121
|
**request_kwargs,
|
2049
|
-
))
|
2122
|
+
)))
|
2050
2123
|
return run_gen_step_iter(gen_step, async_)
|
2051
2124
|
|
2052
2125
|
|
@@ -2692,6 +2765,212 @@ def iter_files_with_path_skim(
|
|
2692
2765
|
return run_gen_step_iter(gen_step, async_)
|
2693
2766
|
|
2694
2767
|
|
2768
|
+
@overload
|
2769
|
+
def traverse_tree(
|
2770
|
+
client: str | P115Client,
|
2771
|
+
cid: int | str = 0,
|
2772
|
+
id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
|
2773
|
+
app: str = "android",
|
2774
|
+
max_workers: None | int = None,
|
2775
|
+
*,
|
2776
|
+
async_: Literal[False] = False,
|
2777
|
+
**request_kwargs,
|
2778
|
+
) -> Iterator[dict]:
|
2779
|
+
...
|
2780
|
+
@overload
|
2781
|
+
def traverse_tree(
|
2782
|
+
client: str | P115Client,
|
2783
|
+
cid: int | str = 0,
|
2784
|
+
id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
|
2785
|
+
app: str = "android",
|
2786
|
+
max_workers: None | int = None,
|
2787
|
+
*,
|
2788
|
+
async_: Literal[True],
|
2789
|
+
**request_kwargs,
|
2790
|
+
) -> AsyncIterator[dict]:
|
2791
|
+
...
|
2792
|
+
def traverse_tree(
|
2793
|
+
client: str | P115Client,
|
2794
|
+
cid: int | str = 0,
|
2795
|
+
id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
|
2796
|
+
app: str = "android",
|
2797
|
+
max_workers: None | int = None,
|
2798
|
+
*,
|
2799
|
+
async_: Literal[False, True] = False,
|
2800
|
+
**request_kwargs,
|
2801
|
+
) -> Iterator[dict] | AsyncIterator[dict]:
|
2802
|
+
"""遍历目录树,获取文件信息
|
2803
|
+
|
2804
|
+
:param client: 115 客户端或 cookies
|
2805
|
+
:param cid: 目录 id 或 pickcode
|
2806
|
+
:param id_to_dirnode: 字典,保存 id 到对应文件的 `DirNode(name, parent_id)` 命名元组的字典
|
2807
|
+
:param app: 使用指定 app(设备)的接口
|
2808
|
+
:param max_workers: 最大并发数,如果为 None 或 <= 0,则自动确定
|
2809
|
+
:param async_: 是否异步
|
2810
|
+
:param request_kwargs: 其它请求参数
|
2811
|
+
|
2812
|
+
:return: 迭代器,返回此目录内的(仅文件)文件信息
|
2813
|
+
"""
|
2814
|
+
if isinstance(client, str):
|
2815
|
+
client = P115Client(client, check_for_relogin=True)
|
2816
|
+
if id_to_dirnode is None:
|
2817
|
+
id_to_dirnode = ID_TO_DIRNODE_CACHE[client.user_id]
|
2818
|
+
elif id_to_dirnode is ...:
|
2819
|
+
id_to_dirnode = {}
|
2820
|
+
from .download import iter_download_nodes
|
2821
|
+
to_pickcode = client.to_pickcode
|
2822
|
+
def fulfill_dir_node(attr: dict, /) -> dict:
|
2823
|
+
attr["pickcode"] = to_pickcode(attr["id"], "fa")
|
2824
|
+
attr["size"] = 0
|
2825
|
+
attr["sha1"] = ""
|
2826
|
+
return attr
|
2827
|
+
def gen_step():
|
2828
|
+
files = iter_download_nodes(
|
2829
|
+
client,
|
2830
|
+
cid,
|
2831
|
+
files=True,
|
2832
|
+
ensure_name=True,
|
2833
|
+
id_to_dirnode=id_to_dirnode,
|
2834
|
+
app=app,
|
2835
|
+
max_workers=max_workers,
|
2836
|
+
async_=async_,
|
2837
|
+
**request_kwargs,
|
2838
|
+
)
|
2839
|
+
with cache_loading(files) as (cache, task):
|
2840
|
+
yield YieldFrom(do_map(fulfill_dir_node, iter_download_nodes(
|
2841
|
+
client,
|
2842
|
+
cid,
|
2843
|
+
files=False,
|
2844
|
+
id_to_dirnode=id_to_dirnode,
|
2845
|
+
app=app,
|
2846
|
+
max_workers=max_workers,
|
2847
|
+
async_=async_,
|
2848
|
+
**request_kwargs,
|
2849
|
+
)))
|
2850
|
+
if isinstance(task, Task):
|
2851
|
+
yield task
|
2852
|
+
else:
|
2853
|
+
task.result()
|
2854
|
+
yield YieldFrom(cache)
|
2855
|
+
yield YieldFrom(files)
|
2856
|
+
return run_gen_step_iter(gen_step, async_)
|
2857
|
+
|
2858
|
+
|
2859
|
+
@overload
|
2860
|
+
def traverse_tree_with_path(
|
2861
|
+
client: str | P115Client,
|
2862
|
+
cid: int | str = 0,
|
2863
|
+
with_ancestors: bool = False,
|
2864
|
+
escape: None | bool | Callable[[str], str] = True,
|
2865
|
+
id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
|
2866
|
+
app: str = "android",
|
2867
|
+
max_workers: None | int = None,
|
2868
|
+
*,
|
2869
|
+
async_: Literal[False] = False,
|
2870
|
+
**request_kwargs,
|
2871
|
+
) -> Iterator[dict]:
|
2872
|
+
...
|
2873
|
+
@overload
|
2874
|
+
def traverse_tree_with_path(
|
2875
|
+
client: str | P115Client,
|
2876
|
+
cid: int | str = 0,
|
2877
|
+
with_ancestors: bool = False,
|
2878
|
+
escape: None | bool | Callable[[str], str] = True,
|
2879
|
+
id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
|
2880
|
+
app: str = "android",
|
2881
|
+
max_workers: None | int = None,
|
2882
|
+
*,
|
2883
|
+
async_: Literal[True],
|
2884
|
+
**request_kwargs,
|
2885
|
+
) -> AsyncIterator[dict]:
|
2886
|
+
...
|
2887
|
+
def traverse_tree_with_path(
|
2888
|
+
client: str | P115Client,
|
2889
|
+
cid: int | str = 0,
|
2890
|
+
with_ancestors: bool = False,
|
2891
|
+
escape: None | bool | Callable[[str], str] = True,
|
2892
|
+
id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
|
2893
|
+
app: str = "android",
|
2894
|
+
max_workers: None | int = None,
|
2895
|
+
*,
|
2896
|
+
async_: Literal[False, True] = False,
|
2897
|
+
**request_kwargs,
|
2898
|
+
) -> Iterator[dict] | AsyncIterator[dict]:
|
2899
|
+
"""遍历目录树,获取文件信息(包含 "path",可选 "ancestors")
|
2900
|
+
|
2901
|
+
:param client: 115 客户端或 cookies
|
2902
|
+
:param cid: 目录 id 或 pickcode
|
2903
|
+
:param with_ancestors: 文件信息中是否要包含 "ancestors"
|
2904
|
+
:param escape: 对文件名进行转义
|
2905
|
+
|
2906
|
+
- 如果为 None,则不处理;否则,这个函数用来对文件名中某些符号进行转义,例如 "/" 等
|
2907
|
+
- 如果为 True,则使用 `posixpatht.escape`,会对文件名中 "/",或单独出现的 "." 和 ".." 用 "\\" 进行转义
|
2908
|
+
- 如果为 False,则使用 `posix_escape_name` 函数对名字进行转义,会把文件名中的 "/" 转换为 "|"
|
2909
|
+
- 如果为 Callable,则用你所提供的调用,以或者转义后的名字
|
2910
|
+
|
2911
|
+
:param id_to_dirnode: 字典,保存 id 到对应文件的 `DirNode(name, parent_id)` 命名元组的字典
|
2912
|
+
:param app: 使用指定 app(设备)的接口
|
2913
|
+
:param max_workers: 最大并发数,如果为 None 或 <= 0,则自动确定
|
2914
|
+
:param async_: 是否异步
|
2915
|
+
:param request_kwargs: 其它请求参数
|
2916
|
+
|
2917
|
+
:return: 迭代器,返回此目录内的(仅文件)文件信息
|
2918
|
+
"""
|
2919
|
+
if isinstance(client, str):
|
2920
|
+
client = P115Client(client, check_for_relogin=True)
|
2921
|
+
if id_to_dirnode is None:
|
2922
|
+
id_to_dirnode = ID_TO_DIRNODE_CACHE[client.user_id]
|
2923
|
+
elif id_to_dirnode is ...:
|
2924
|
+
id_to_dirnode = {}
|
2925
|
+
from .download import iter_download_nodes
|
2926
|
+
to_pickcode = client.to_pickcode
|
2927
|
+
def fulfill_dir_node(attr: dict, /) -> dict:
|
2928
|
+
attr["pickcode"] = to_pickcode(attr["id"], "fa")
|
2929
|
+
attr["size"] = 0
|
2930
|
+
attr["sha1"] = ""
|
2931
|
+
return attr
|
2932
|
+
def gen_step():
|
2933
|
+
files = iter_download_nodes(
|
2934
|
+
client,
|
2935
|
+
cid,
|
2936
|
+
files=True,
|
2937
|
+
ensure_name=True,
|
2938
|
+
id_to_dirnode=id_to_dirnode,
|
2939
|
+
app=app,
|
2940
|
+
max_workers=max_workers,
|
2941
|
+
async_=async_,
|
2942
|
+
**request_kwargs,
|
2943
|
+
)
|
2944
|
+
with cache_loading(files) as (cache, task):
|
2945
|
+
yield YieldFrom(do_map(fulfill_dir_node, iter_dirs_with_path(
|
2946
|
+
client,
|
2947
|
+
cid,
|
2948
|
+
with_ancestors=with_ancestors,
|
2949
|
+
escape=escape,
|
2950
|
+
id_to_dirnode=id_to_dirnode,
|
2951
|
+
app=app,
|
2952
|
+
max_workers=max_workers,
|
2953
|
+
async_=async_,
|
2954
|
+
**request_kwargs,
|
2955
|
+
)))
|
2956
|
+
if isinstance(task, Task):
|
2957
|
+
yield task
|
2958
|
+
else:
|
2959
|
+
task.result()
|
2960
|
+
add_top = _make_top_adder(to_id(cid), id_to_dirnode)
|
2961
|
+
yield YieldFrom(do_map(add_top, ensure_attr_path(
|
2962
|
+
client,
|
2963
|
+
chain(cache, files), # type: ignore
|
2964
|
+
with_ancestors=with_ancestors,
|
2965
|
+
escape=escape,
|
2966
|
+
id_to_dirnode=id_to_dirnode,
|
2967
|
+
app=app,
|
2968
|
+
async_=async_,
|
2969
|
+
**request_kwargs,
|
2970
|
+
)))
|
2971
|
+
return run_gen_step_iter(gen_step, async_)
|
2972
|
+
|
2973
|
+
|
2695
2974
|
@overload
|
2696
2975
|
def iter_nodes(
|
2697
2976
|
client: str | P115Client,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "p115client"
|
3
|
-
version = "0.0.5.14.
|
3
|
+
version = "0.0.5.14.3"
|
4
4
|
description = "Python 115 webdisk client."
|
5
5
|
authors = ["ChenyangGao <wosiwujm@gmail.com>"]
|
6
6
|
license = "MIT"
|
@@ -49,7 +49,7 @@ python-filewrap = ">=0.2.8"
|
|
49
49
|
python-hashtools = ">=0.0.3.3"
|
50
50
|
python-httpfile = ">=0.0.5.2"
|
51
51
|
python-http_request = ">=0.0.6"
|
52
|
-
python-iterutils = ">=0.2.5.
|
52
|
+
python-iterutils = ">=0.2.5.4"
|
53
53
|
python-property = ">=0.0.3"
|
54
54
|
python-startfile = ">=0.0.2"
|
55
55
|
python-undefined = ">=0.0.3"
|
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
|