p115client 0.0.5.14.2__tar.gz → 0.0.5.14.4__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.
Files changed (27) hide show
  1. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/PKG-INFO +3 -3
  2. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/client.py +8 -4
  3. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/iterdir.py +290 -11
  4. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/pyproject.toml +3 -3
  5. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/LICENSE +0 -0
  6. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/__init__.py +0 -0
  7. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/_upload.py +0 -0
  8. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/const.py +0 -0
  9. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/exception.py +0 -0
  10. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/py.typed +0 -0
  11. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/__init__.py +0 -0
  12. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/attr.py +0 -0
  13. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/auth.py +0 -0
  14. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/download.py +0 -0
  15. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/edit.py +0 -0
  16. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/export_dir.py +0 -0
  17. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/fs_files.py +0 -0
  18. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/history.py +0 -0
  19. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/life.py +0 -0
  20. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/offline.py +0 -0
  21. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/pool.py +0 -0
  22. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/request.py +0 -0
  23. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/upload.py +0 -0
  24. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/util.py +0 -0
  25. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/tool/xys.py +0 -0
  26. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/p115client/type.py +0 -0
  27. {p115client-0.0.5.14.2 → p115client-0.0.5.14.4}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p115client
3
- Version: 0.0.5.14.2
3
+ Version: 0.0.5.14.4
4
4
  Summary: Python 115 webdisk client.
5
5
  Home-page: https://github.com/ChenyangGao/p115client
6
6
  License: MIT
@@ -29,7 +29,7 @@ Requires-Dist: iter_collect (>=0.0.5.1)
29
29
  Requires-Dist: multidict
30
30
  Requires-Dist: orjson
31
31
  Requires-Dist: p115cipher (>=0.0.3)
32
- Requires-Dist: p115pickcode (>=0.0.4)
32
+ Requires-Dist: p115pickcode (>=0.0.5)
33
33
  Requires-Dist: posixpatht (>=0.0.3)
34
34
  Requires-Dist: python-argtools (>=0.0.1)
35
35
  Requires-Dist: python-asynctools (>=0.1.3)
@@ -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.3)
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)
@@ -693,7 +693,8 @@ def normalize_attr_web[D: dict[str, Any]](
693
693
  if "te" in info:
694
694
  attr["mtime"] = int(info["te"])
695
695
  else:
696
- attr["pick_code"] = attr["pickcode"]
696
+ if "pickcode" in attr:
697
+ attr["pick_code"] = attr["pickcode"]
697
698
  attr["ico"] = info.get("ico", "folder" if is_dir else "")
698
699
  if "te" in info:
699
700
  attr["mtime"] = attr["user_utime"] = int(info["te"])
@@ -837,7 +838,8 @@ def normalize_attr_app[D: dict[str, Any]](
837
838
  if "upt" in info:
838
839
  attr["mtime"] = int(info["upt"])
839
840
  else:
840
- attr["pick_code"] = attr["pickcode"]
841
+ if "pickcode" in attr:
842
+ attr["pick_code"] = attr["pickcode"]
841
843
  attr["ico"] = info.get("ico", "folder" if attr["is_dir"] else "")
842
844
  if "thumb" in info:
843
845
  thumb = info["thumb"]
@@ -980,7 +982,8 @@ def normalize_attr_app2[D: dict[str, Any]](
980
982
  if "user_ptime" in info:
981
983
  attr["mtime"] = int(info["user_ptime"])
982
984
  else:
983
- attr["pick_code"] = attr["pickcode"]
985
+ if "pickcode" in attr:
986
+ attr["pick_code"] = attr["pickcode"]
984
987
  if is_dir:
985
988
  if "thumb_url" in info:
986
989
  attr["thumb"] = info["thumb_url"]
@@ -1620,7 +1623,7 @@ class ClientRequestMixin:
1620
1623
  api = complete_api("/open/authorize", base_url=base_url)
1621
1624
  payload = {"response_type": "code", **payload}
1622
1625
  def parse(resp, content, /):
1623
- if resp.status_code == 302:
1626
+ if get_status_code(resp) == 302:
1624
1627
  return {
1625
1628
  "state": True,
1626
1629
  "url": resp.headers["location"],
@@ -2290,6 +2293,7 @@ class ClientRequestMixin:
2290
2293
 
2291
2294
  :param app: 扫二维码后绑定的 `app` (或者叫 `device`)
2292
2295
  :param console_qrcode: 在命令行输出二维码,否则在浏览器中打开
2296
+ :param base_url: 接口的基地址
2293
2297
  :param async_: 是否异步
2294
2298
  :param request_kwargs: 其它请求参数
2295
2299
 
@@ -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
- "iter_nodes", "iter_nodes_skim", "iter_nodes_by_pickcode",
15
- "iter_nodes_using_update", "iter_nodes_using_info",
16
- "iter_nodes_using_star_event", "iter_dir_nodes_using_star",
17
- "iter_parents", "iter_files_shortcut", "iter_dupfiles", "iter_image_files",
18
- "search_iter", "share_iterdir", "share_iter_files", "share_search_iter",
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, Iterable,
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
- return YieldFrom(ensure_attr_path(
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.2"
3
+ version = "0.0.5.14.4"
4
4
  description = "Python 115 webdisk client."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
@@ -37,7 +37,7 @@ iter_collect = ">=0.0.5.1"
37
37
  multidict = "*"
38
38
  orjson = "*"
39
39
  p115cipher = ">=0.0.3"
40
- p115pickcode = ">=0.0.4"
40
+ p115pickcode = ">=0.0.5"
41
41
  posixpatht = ">=0.0.3"
42
42
  python-argtools = ">=0.0.1"
43
43
  python-asynctools = ">=0.1.3"
@@ -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.3"
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