p123client 0.0.7.1__tar.gz → 0.0.7.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.1
2
2
  Name: p123client
3
- Version: 0.0.7.1
3
+ Version: 0.0.7.2
4
4
  Summary: Python 123 webdisk client.
5
5
  Home-page: https://github.com/ChenyangGao/p123client
6
6
  License: MIT
@@ -7,6 +7,7 @@ __all__ = ["check_response", "P123OpenClient", "P123Client"]
7
7
 
8
8
  import errno
9
9
 
10
+ from asyncio import Lock as AsyncLock
10
11
  from base64 import urlsafe_b64decode
11
12
  from collections.abc import (
12
13
  AsyncIterable, Awaitable, Buffer, Callable, Coroutine,
@@ -17,7 +18,7 @@ from functools import partial
17
18
  from hashlib import md5
18
19
  from http.cookiejar import CookieJar
19
20
  from inspect import isawaitable
20
- from itertools import chain
21
+ from itertools import chain, count
21
22
  from os import fsdecode, fstat, isatty, PathLike
22
23
  from os.path import basename
23
24
  from pathlib import Path, PurePath
@@ -25,6 +26,7 @@ from re import compile as re_compile, MULTILINE
25
26
  from string import digits, hexdigits, ascii_uppercase
26
27
  from sys import _getframe
27
28
  from tempfile import TemporaryFile
29
+ from threading import Lock
28
30
  from typing import cast, overload, Any, Final, Literal, Self
29
31
  from urllib.parse import parse_qsl, urlsplit
30
32
  from uuid import uuid4
@@ -48,7 +50,8 @@ from yarl import URL
48
50
 
49
51
  from .const import CLIENT_API_METHODS_MAP, CLIENT_METHOD_API_MAP
50
52
  from .exception import (
51
- P123OSError, P123BrokenUpload, P123LoginError, P123AuthenticationError, P123FileNotFoundError,
53
+ P123Warning, P123OSError, P123BrokenUpload, P123LoginError,
54
+ P123AuthenticationError, P123FileNotFoundError,
52
55
  )
53
56
 
54
57
 
@@ -210,48 +213,128 @@ def check_response(resp: dict | Awaitable[dict], /) -> dict | Coroutine[Any, Any
210
213
 
211
214
 
212
215
  class P123OpenClient:
213
- """123 网盘客户端
216
+ """123 网盘客户端,仅使用开放接口
214
217
 
215
218
  .. admonition:: Reference
216
219
 
217
220
  https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced
218
- """
219
221
 
222
+ :param client_id: 应用标识,创建应用时分配的 appId
223
+ :param client_secret: 应用密钥,创建应用时分配的 secretId
224
+ :param token: 123 的访问令牌
225
+ :param refresh_token: 刷新令牌
226
+ :param check_for_relogin: 当 access_token 失效时,是否重新登录
227
+ """
220
228
  client_id: str = ""
221
229
  client_secret: str = ""
222
230
  refresh_token: str = ""
223
231
  token_path: None | PurePath = None
232
+ check_for_relogin: bool = False
224
233
 
225
234
  def __init__(
226
- self, /,
235
+ self,
236
+ /,
227
237
  client_id: str | PathLike = "",
228
238
  client_secret: str = "",
229
239
  token: None | str | PathLike = None,
230
240
  refresh_token: str = "",
241
+ check_for_relogin: bool = True,
231
242
  ):
232
- if isinstance(client_id, PathLike):
233
- token = client_id
234
- else:
235
- self.client_id = client_id
236
- self.client_secret = client_secret
237
- self.refresh_token = refresh_token
238
- if token is None:
239
- if client_id and client_secret or refresh_token:
240
- self.login_open()
241
- elif isinstance(token, str):
242
- self.token = token.removeprefix("Bearer ")
243
- else:
244
- if isinstance(token, PurePath) and hasattr(token, "open"):
245
- self.token_path = token
246
- else:
247
- self.token_path = Path(fsdecode(token))
248
- self._read_token()
249
- if not self.token and (client_id and client_secret or refresh_token):
250
- self.login_open()
243
+ self.init(
244
+ client_id=client_id,
245
+ client_secret=client_secret,
246
+ token=token,
247
+ refresh_token=refresh_token,
248
+ check_for_relogin=check_for_relogin,
249
+ instance=self,
250
+ )
251
251
 
252
252
  def __del__(self, /):
253
253
  self.close()
254
254
 
255
+ @overload
256
+ @classmethod
257
+ def init(
258
+ cls,
259
+ /,
260
+ client_id: str | PathLike = "",
261
+ client_secret: str = "",
262
+ token: None | str | PathLike = None,
263
+ refresh_token: str = "",
264
+ check_for_relogin: bool = True,
265
+ instance: None | Self = None,
266
+ *,
267
+ async_: Literal[False] = False,
268
+ **request_kwargs,
269
+ ) -> P123OpenClient:
270
+ ...
271
+ @overload
272
+ @classmethod
273
+ def init(
274
+ cls,
275
+ /,
276
+ client_id: str | PathLike = "",
277
+ client_secret: str = "",
278
+ token: None | str | PathLike = None,
279
+ refresh_token: str = "",
280
+ check_for_relogin: bool = True,
281
+ instance: None | Self = None,
282
+ *,
283
+ async_: Literal[True],
284
+ **request_kwargs,
285
+ ) -> Coroutine[Any, Any, P123OpenClient]:
286
+ ...
287
+ @classmethod
288
+ def init(
289
+ cls,
290
+ /,
291
+ client_id: str | PathLike = "",
292
+ client_secret: str = "",
293
+ token: None | str | PathLike = None,
294
+ refresh_token: str = "",
295
+ check_for_relogin: bool = True,
296
+ instance: None | Self = None,
297
+ *,
298
+ async_: Literal[False, True] = False,
299
+ **request_kwargs,
300
+ ) -> P123OpenClient | Coroutine[Any, Any, P123OpenClient]:
301
+ def gen_step():
302
+ nonlocal token
303
+ if instance is None:
304
+ self = cls.__new__(cls)
305
+ else:
306
+ self = instance
307
+ if isinstance(client_id, PathLike):
308
+ token = client_id
309
+ else:
310
+ self.client_id = client_id
311
+ self.client_secret = client_secret
312
+ self.refresh_token = refresh_token
313
+ if token is None:
314
+ if client_id and client_secret or refresh_token:
315
+ yield self.login_open(async_=async_, **request_kwargs)
316
+ elif isinstance(token, str):
317
+ self.token = token.removeprefix("Bearer ")
318
+ else:
319
+ if isinstance(token, PurePath) and hasattr(token, "open"):
320
+ self.token_path = token
321
+ else:
322
+ self.token_path = Path(fsdecode(token))
323
+ self._read_token()
324
+ if not self.token and (client_id and client_secret or refresh_token):
325
+ yield self.login_open(async_=async_, **request_kwargs)
326
+ self.check_for_relogin = check_for_relogin
327
+ return self
328
+ return run_gen_step(gen_step, async_)
329
+
330
+ @locked_cacheproperty
331
+ def request_lock(self, /) -> Lock:
332
+ return Lock()
333
+
334
+ @locked_cacheproperty
335
+ def request_alock(self, /) -> AsyncLock:
336
+ return AsyncLock()
337
+
255
338
  @property
256
339
  def cookies(self, /):
257
340
  """请求所用的 Cookies 对象(同步和异步共用)
@@ -380,6 +463,12 @@ class P123OpenClient:
380
463
  self.__dict__.pop("session", None)
381
464
  self.__dict__.pop("async_session", None)
382
465
 
466
+ def can_relogin(self, /) -> bool:
467
+ return self.check_for_relogin and bool(
468
+ self.client_id and self.client_secret or
469
+ getattr(self, "refresh_token")
470
+ )
471
+
383
472
  def request(
384
473
  self,
385
474
  /,
@@ -397,22 +486,57 @@ class P123OpenClient:
397
486
  request_kwargs.setdefault("parse", default_parse)
398
487
  if request is None:
399
488
  request_kwargs["session"] = self.async_session if async_ else self.session
400
- return get_default_request()(
401
- url=url,
402
- method=method,
403
- async_=async_,
404
- **request_kwargs,
405
- )
489
+ request_kwargs["async_"] = async_
490
+ request = get_default_request()
491
+ if self.can_relogin():
492
+ headers = dict(self.headers)
493
+ if request_headers := request_kwargs.get("headers"):
494
+ headers.update(request_headers)
495
+ headers.setdefault("authorization", "")
496
+ request_kwargs["headers"] = headers
406
497
  else:
407
- if headers := request_kwargs.get("headers"):
408
- request_kwargs["headers"] = {**self.headers, **headers}
409
- else:
410
- request_kwargs["headers"] = self.headers
411
498
  return request(
412
499
  url=url,
413
500
  method=method,
414
501
  **request_kwargs,
415
502
  )
503
+ def gen_step():
504
+ if async_:
505
+ lock: Lock | AsyncLock = self.request_alock
506
+ else:
507
+ lock = self.request_lock
508
+ headers = request_kwargs["headers"]
509
+ if "authorization" not in headers:
510
+ headers["authorization"] = "Bearer " + self.token
511
+ for i in count(0):
512
+ token = headers["authorization"].removeprefix("Bearer ")
513
+ resp = yield cast(Callable, request)(
514
+ url=url,
515
+ method=method,
516
+ **request_kwargs,
517
+ )
518
+ if not (isinstance(resp, dict) and resp.get("code") == 401):
519
+ return resp
520
+ yield lock.acquire()
521
+ try:
522
+ token_new: str = self.token
523
+ if token == token_new:
524
+ if self.__dict__.get("token_path"):
525
+ token_new = self._read_token() or ""
526
+ if token != token_new:
527
+ headers["authorization"] = "Bearer " + self.token
528
+ continue
529
+ if i:
530
+ raise
531
+ user_id = getattr(self, "user_id", None)
532
+ warn(f"relogin to refresh token: {user_id=}", category=P123Warning)
533
+ yield self.login(replace=True, async_=async_)
534
+ headers["authorization"] = "Bearer " + self.token
535
+ else:
536
+ headers["authorization"] = "Bearer " + token_new
537
+ finally:
538
+ lock.release()
539
+ return run_gen_step(gen_step, async_)
416
540
 
417
541
  @overload
418
542
  def login(
@@ -5549,52 +5673,132 @@ class P123Client(P123OpenClient):
5549
5673
  client_id: str = "",
5550
5674
  client_secret: str = "",
5551
5675
  refresh_token: str = "",
5676
+ check_for_relogin: bool = True,
5552
5677
  ):
5553
- if (isinstance(passport, PathLike) or
5554
- not token and
5555
- isinstance(passport, str) and
5556
- len(passport) >= 128
5557
- ):
5558
- token = passport
5559
- elif (not refresh_token and
5560
- isinstance(passport, str) and
5561
- len(passport) >= 48 and
5562
- not passport.strip(digits+ascii_uppercase)
5563
- ):
5564
- refresh_token = passport
5565
- elif (not client_id and
5566
- isinstance(passport, str) and
5567
- len(passport) >= 32 and
5568
- not passport.strip(digits+"abcdef")
5569
- ):
5570
- client_id = passport
5571
- else:
5572
- self.passport = passport
5573
- if (not client_secret and
5574
- isinstance(password, str)
5575
- and len(password) >= 32 and
5576
- not password.strip(digits+"abcdef")
5577
- ):
5578
- client_secret = password
5579
- else:
5580
- self.password = password
5581
- self.client_id = client_id
5582
- self.client_secret = client_secret
5583
- self.refresh_token = refresh_token
5584
- if token is None:
5585
- self.login()
5586
- elif isinstance(token, str):
5587
- self.token = token.removeprefix("Bearer ")
5588
- else:
5589
- if isinstance(token, PurePath) and hasattr(token, "open"):
5590
- self.token_path = token
5678
+ self.init(
5679
+ passport=passport,
5680
+ password=password,
5681
+ token=token,
5682
+ client_id=client_id,
5683
+ client_secret=client_secret,
5684
+ refresh_token=refresh_token,
5685
+ check_for_relogin=check_for_relogin,
5686
+ instance=self,
5687
+ )
5688
+
5689
+ @overload # type: ignore
5690
+ @classmethod
5691
+ def init(
5692
+ cls,
5693
+ /,
5694
+ passport: int | str | PathLike = "",
5695
+ password: str = "",
5696
+ token: None | str | PathLike = None,
5697
+ client_id: str = "",
5698
+ client_secret: str = "",
5699
+ refresh_token: str = "",
5700
+ check_for_relogin: bool = True,
5701
+ instance: None | Self = None,
5702
+ *,
5703
+ async_: Literal[False] = False,
5704
+ **request_kwargs,
5705
+ ) -> P123Client:
5706
+ ...
5707
+ @overload
5708
+ @classmethod
5709
+ def init(
5710
+ cls,
5711
+ /,
5712
+ passport: int | str | PathLike = "",
5713
+ password: str = "",
5714
+ token: None | str | PathLike = None,
5715
+ client_id: str = "",
5716
+ client_secret: str = "",
5717
+ refresh_token: str = "",
5718
+ check_for_relogin: bool = True,
5719
+ instance: None | Self = None,
5720
+ *,
5721
+ async_: Literal[True],
5722
+ **request_kwargs,
5723
+ ) -> Coroutine[Any, Any, P123Client]:
5724
+ ...
5725
+ @classmethod
5726
+ def init(
5727
+ cls,
5728
+ /,
5729
+ passport: int | str | PathLike = "",
5730
+ password: str = "",
5731
+ token: None | str | PathLike = None,
5732
+ client_id: str = "",
5733
+ client_secret: str = "",
5734
+ refresh_token: str = "",
5735
+ check_for_relogin: bool = True,
5736
+ instance: None | Self = None,
5737
+ *,
5738
+ async_: Literal[False, True] = False,
5739
+ **request_kwargs,
5740
+ ) -> P123Client | Coroutine[Any, Any, P123Client]:
5741
+ def gen_step():
5742
+ nonlocal token, refresh_token, client_id, client_secret
5743
+ if instance is None:
5744
+ self = cls.__new__(cls)
5745
+ else:
5746
+ self = instance
5747
+ if (isinstance(passport, PathLike) or
5748
+ not token and
5749
+ isinstance(passport, str) and
5750
+ len(passport) >= 128
5751
+ ):
5752
+ token = passport
5753
+ elif (not refresh_token and
5754
+ isinstance(passport, str) and
5755
+ len(passport) >= 48 and
5756
+ not passport.strip(digits+ascii_uppercase)
5757
+ ):
5758
+ refresh_token = passport
5759
+ elif (not client_id and
5760
+ isinstance(passport, str) and
5761
+ len(passport) >= 32 and
5762
+ not passport.strip(digits+"abcdef")
5763
+ ):
5764
+ client_id = passport
5765
+ else:
5766
+ self.passport = passport
5767
+ if (not client_secret and
5768
+ isinstance(password, str)
5769
+ and len(password) >= 32 and
5770
+ not password.strip(digits+"abcdef")
5771
+ ):
5772
+ client_secret = password
5773
+ else:
5774
+ self.password = password
5775
+ self.client_id = client_id
5776
+ self.client_secret = client_secret
5777
+ self.refresh_token = refresh_token
5778
+ if token is None:
5779
+ yield self.login(async_=async_, **request_kwargs)
5780
+ elif isinstance(token, str):
5781
+ self.token = token.removeprefix("Bearer ")
5591
5782
  else:
5592
- self.token_path = Path(fsdecode(token))
5593
- self._read_token()
5594
- if not self.token:
5595
- self.login()
5596
- if not self.passport:
5597
- self.passport = self.token_user_info["username"]
5783
+ if isinstance(token, PurePath) and hasattr(token, "open"):
5784
+ self.token_path = token
5785
+ else:
5786
+ self.token_path = Path(fsdecode(token))
5787
+ self._read_token()
5788
+ if not self.token:
5789
+ yield self.login(async_=async_, **request_kwargs)
5790
+ if not self.passport:
5791
+ self.passport = self.token_user_info["username"]
5792
+ self.check_for_relogin = check_for_relogin
5793
+ return self
5794
+ return run_gen_step(gen_step, async_)
5795
+
5796
+ def can_relogin(self, /) -> bool:
5797
+ return self.check_for_relogin and bool(
5798
+ self.passport and self.password or
5799
+ self.client_id and self.client_secret or
5800
+ getattr(self, "refresh_token")
5801
+ )
5598
5802
 
5599
5803
  @overload # type: ignore
5600
5804
  def login(
@@ -8476,7 +8680,7 @@ class P123Client(P123OpenClient):
8476
8680
  @overload
8477
8681
  def offline_task_list(
8478
8682
  self,
8479
- payload: dict | int = 1,
8683
+ payload: dict | int | list[int] | tuple[int] = 1,
8480
8684
  /,
8481
8685
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8482
8686
  *,
@@ -8487,7 +8691,7 @@ class P123Client(P123OpenClient):
8487
8691
  @overload
8488
8692
  def offline_task_list(
8489
8693
  self,
8490
- payload: dict | int = 1,
8694
+ payload: dict | int | list[int] | tuple[int] = 1,
8491
8695
  /,
8492
8696
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8493
8697
  *,
@@ -8497,7 +8701,7 @@ class P123Client(P123OpenClient):
8497
8701
  ...
8498
8702
  def offline_task_list(
8499
8703
  self,
8500
- payload: dict | int = 1,
8704
+ payload: dict | int | list[int] | tuple[int] = 1,
8501
8705
  /,
8502
8706
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8503
8707
  *,
@@ -8514,9 +8718,10 @@ class P123Client(P123OpenClient):
8514
8718
  - status_arr: list[ 0 | 1 | 2 | 3 | 4 ] = [0, 1, 2, 3, 4] 💡 状态列表:0:进行中 1:下载失败 2:下载成功 3:重试中
8515
8719
  """
8516
8720
  if isinstance(payload, int):
8517
- payload = {"current_page": payload, "page_size": 100, "status_arr": [0, 1, 2, 3, 4]}
8518
- else:
8519
- payload = {"current_page": 1, "page_size": 100, "status_arr": [0, 1, 2, 3, 4], **payload}
8721
+ payload = {"current_page": payload}
8722
+ elif isinstance(payload, (list, tuple)):
8723
+ payload = { "status_arr": payload}
8724
+ payload = {"current_page": 1, "page_size": 100, "status_arr": [0, 1, 2, 3, 4], **payload}
8520
8725
  return self.request(
8521
8726
  "offline_download/task/list",
8522
8727
  "POST",
@@ -8583,6 +8788,7 @@ class P123Client(P123OpenClient):
8583
8788
  self,
8584
8789
  payload: dict | Iterable[dict],
8585
8790
  /,
8791
+ upload_dir: None | int | str = None,
8586
8792
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8587
8793
  *,
8588
8794
  async_: Literal[False] = False,
@@ -8594,6 +8800,7 @@ class P123Client(P123OpenClient):
8594
8800
  self,
8595
8801
  payload: dict | Iterable[dict],
8596
8802
  /,
8803
+ upload_dir: None | int | str = None,
8597
8804
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8598
8805
  *,
8599
8806
  async_: Literal[True],
@@ -8604,6 +8811,7 @@ class P123Client(P123OpenClient):
8604
8811
  self,
8605
8812
  payload: dict | Iterable[dict],
8606
8813
  /,
8814
+ upload_dir: None | int | str = None,
8607
8815
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8608
8816
  *,
8609
8817
  async_: Literal[False, True] = False,
@@ -8644,6 +8852,9 @@ class P123Client(P123OpenClient):
8644
8852
  "select_file_id": [info["id"] for info in resource["files"]],
8645
8853
  } for resource in payload]
8646
8854
  }
8855
+ payload = cast(dict, payload)
8856
+ if upload_dir is not None:
8857
+ payload["upload_dir"] = upload_dir
8647
8858
  return self.request(
8648
8859
  "v2/offline_download/task/submit",
8649
8860
  "POST",
@@ -8706,6 +8917,7 @@ class P123Client(P123OpenClient):
8706
8917
  self,
8707
8918
  /,
8708
8919
  url: str | Iterable[str],
8920
+ upload_dir: None | int | str = None,
8709
8921
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8710
8922
  *,
8711
8923
  async_: Literal[False] = False,
@@ -8717,6 +8929,7 @@ class P123Client(P123OpenClient):
8717
8929
  self,
8718
8930
  /,
8719
8931
  url: str | Iterable[str],
8932
+ upload_dir: None | int | str = None,
8720
8933
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8721
8934
  *,
8722
8935
  async_: Literal[True],
@@ -8727,6 +8940,7 @@ class P123Client(P123OpenClient):
8727
8940
  self,
8728
8941
  /,
8729
8942
  url: str | Iterable[str],
8943
+ upload_dir: None | int | str = None,
8730
8944
  base_url: str | Callable[[], str] = DEFAULT_BASE_URL,
8731
8945
  *,
8732
8946
  async_: Literal[False, True] = False,
@@ -8737,6 +8951,7 @@ class P123Client(P123OpenClient):
8737
8951
  POST https://www.123pan.com/api/offline_download/upload/seed
8738
8952
 
8739
8953
  :param url: info_hash(只允许单个)、下载链接(多个用 "\n" 分隔)或者多个下载链接的迭代器
8954
+ :param upload_dir: 保存到目录的 id
8740
8955
  :param base_url: API 链接的基地址
8741
8956
  :param async_: 是否异步
8742
8957
  :param request_kwargs: 其它请求参数
@@ -8760,6 +8975,7 @@ class P123Client(P123OpenClient):
8760
8975
  check_response(resp)
8761
8976
  return self.offline_task_submit(
8762
8977
  resp["data"]["list"],
8978
+ upload_dir,
8763
8979
  base_url=base_url,
8764
8980
  async_=async_,
8765
8981
  **request_kwargs,
@@ -10502,4 +10718,3 @@ with temp_globals():
10502
10718
  except KeyError:
10503
10719
  CLIENT_API_METHODS_MAP[api] = [name]
10504
10720
 
10505
- # TODO: 实现 check_for_relogin 参数,当报错 401,则重新登录(如果用的是 client_id,账号密码 或 refresh_token),调用 client.login()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "p123client"
3
- version = "0.0.7.1"
3
+ version = "0.0.7.2"
4
4
  description = "Python 123 webdisk client."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
File without changes
File without changes