Source code for p115client.type

#!/usr/bin/env python3
# encoding: utf-8

__author__ = "ChenyangGao <https://chenyanggao.github.io>"
__all__ = [
    "DirNode", "P115UID", "P115Cookies", "P115DictAttrLikeMixin", 
    "P115DictAttrLike", "P115ID", "P115StrID", "P115URL", 
    "TaskResultTuple", 
]

from datetime import datetime, timedelta
from functools import cached_property
from http.cookiejar import CookieJar, Cookie
from http.cookies import BaseCookie, Morsel
from re import compile as re_compile
from time import time
from types import MappingProxyType
from typing import Any, Final, NamedTuple, Self

from cookietools import cookies_to_dict, cookies_to_str
from integer_tool import try_parse_int
from undefined import undefined

from .const import SSOENT_TO_APP


CRE_UID_FORMAT_match: Final = re_compile("(?P<user_id>[1-9][0-9]*)_(?P<login_ssoent>[A-Z][1-9][0-9]*)_(?P<login_timestamp>[1-9][0-9]{9,})").fullmatch
CRE_CID_FORMAT_match: Final = re_compile("[0-9a-f]{32}").fullmatch
CRE_SEID_FORMAT_match: Final = re_compile("[0-9a-f]{120}").fullmatch


[docs] class DirNode(NamedTuple): """用来保存某个 id 对应的 name 和 parent_id 的元组 """ name: str parent_id: int
[docs] class P115UID(str): def __init__(self, /, *a, **k): if m := CRE_UID_FORMAT_match(self): self.__dict__.update((k, try_parse_int(v)) for k, v in m.groupdict().items()) def __repr__(self, /) -> str: cls = type(self) return f"{cls.__module__}.{cls.__qualname__}({super().__repr__()})" @cached_property def user_id(self, /) -> int: return 0 @cached_property def login_ssoent(self, /) -> str: return "" @cached_property def login_timestamp(self, /) -> int: return 0
[docs] class P115Cookies(str): """cookies 的封装 """ __last_new_instance__ = None def __new__(cls, cookies, /): def predicate(_, val, /): domain: None | str = None if isinstance(val, Cookie): domain = val.domain elif isinstance(val, Morsel): domain = val["domain"] return not domain or domain == "115.com" or domain.endswith(".115.com") cookies = cookies_to_str(cookies, predicate) if cookies == cls.__last_new_instance__: return cls.__last_new_instance__ else: inst = cls.__last_new_instance__ = super().__new__(cls, cookies) return inst def __deepcopy__(self, /, memo) -> Self: return self def __getattr__(self, attr: str, /): try: return self.mapping[attr] except KeyError as e: raise AttributeError(attr) from e def __getitem__(self, key, /): # type: ignore if isinstance(key, str): return self.mapping[key] return super().__getitem__(key) def __repr__(self, /) -> str: cls = type(self) return f"{cls.__module__}.{cls.__qualname__}({super().__repr__()})" def __setattr__(self, attr, value, /): raise TypeError("can't set attribute") @cached_property def mapping(self, /) -> MappingProxyType: return MappingProxyType(cookies_to_dict(self)) @cached_property def uid(self, /) -> P115UID: "UID 字段" return P115UID(self.mapping.get("UID") or "") @cached_property def cid(self, /) -> str: "CID 字段" return self.mapping.get("CID") or "" @cached_property def kid(self, /) -> str: "KID 字段" return self.mapping.get("KID") or "" @cached_property def seid(self, /) -> str: "SEID 字段" return self.mapping.get("SEID") or "" @property def user_id(self, /) -> int: "用户 ID" return self.uid.user_id @property def login_ssoent(self, /) -> str: "登录设备标识" return self.uid.login_ssoent ssoent = login_ssoent @cached_property def login_app(self, /) -> None | str: "登录设备名" return SSOENT_TO_APP.get(self.login_ssoent) @property def app(self, /) -> None | str: "登录设备名" return self.login_app @property def login_timestamp(self, /) -> int: "登录时间戳" return self.uid.login_timestamp timestamp = login_timestamp @cached_property def datetime(self, /) -> datetime: "登录时间" return datetime.fromtimestamp(self.login_timestamp) @property def time_elapsed(self, /) -> float: "从登录到现在过了多少时间,单位:秒" return time() - self.login_timestamp @property def time_delta(self, /) -> timedelta: "从登录到现在过了多少时间" return datetime.now() - self.datetime @cached_property def is_well_formed(self, /) -> bool: "是否格式良好,即需要的字段都具备而且都符合格式" return ( CRE_UID_FORMAT_match(self.uid) and CRE_CID_FORMAT_match(self.cid) and CRE_SEID_FORMAT_match(self.seid) ) is not None @cached_property def cookies(self, /) -> str: """115 登录的 cookies,包含 UID、CID、KID 和 SEID 这 4 个字段 """ cookies = f"UID={self.uid}; CID={self.cid}; SEID={self.seid}" if "KID" in self.mapping: cookies += f"; KID={self.mapping['KID']}" return cookies @classmethod def from_dict(cls, cookies: dict[str, str], /) -> Self: return cls("; ".join(f"{key}={val}" for key, val in cookies.items())) @classmethod def from_cookiejar(cls, cookiejar: CookieJar, /) -> Self: return cls("; ".join( f"{cookie.name}={cookie.value}" for cookie in cookiejar if not (domain := cookie.domain) or domain == "115.com" or domain.endswith(".115.com") )) @classmethod def from_simple_cookie(cls, cookies: BaseCookie, /) -> Self: return cls("; ".join( f"{name}={cookie.value}" for name, cookie in cookies.items() if not (domain := cookie["domain"]) or domain == "115.com" or domain.endswith(".115.com") ))
class P115DictAttrLikeMixin: def __getattr__(self, attr: str, /): try: return self.__dict__[attr] except KeyError as e: raise AttributeError(attr) from e def __delitem__(self, key: str, /): del self.__dict__[key] def __getitem__(self, key, /): try: if isinstance(key, str): return self.__dict__[key] except KeyError: return super().__getitem__(key) # type: ignore def __setitem__(self, key: str, val, /): self.__dict__[key] = val def __repr__(self, /) -> str: cls = type(self) if (module := cls.__module__) not in ("__main__", "builtins"): name = cls.__qualname__ else: name = f"{module}.{cls.__qualname__}" return f"{name}({super().__repr__()}, {self.__dict__!r})" @property def mapping(self, /) -> dict[str, Any]: return self.__dict__ def get(self, key, /, default=None): return self.__dict__.get(key, default) def items(self, /): return self.__dict__.items() def keys(self, /): return self.__dict__.keys() def values(self, /): return self.__dict__.values() class P115DictAttrLike(P115DictAttrLikeMixin): def __new__(cls, val: Any = undefined, /, *args, **kwds): if val is undefined: return super().__new__(cls) else: return super().__new__(cls, val) # type: ignore def __init__(self, val: Any = undefined, /, *args, **kwds): self.__dict__.update(*args, **kwds) @classmethod def of(cls, val: Any = undefined, /, ns: None | dict = None) -> Self: if val is undefined: self = cls.__new__(cls) else: self = cls.__new__(cls, val) if ns is not None: self.__dict__ = ns return self @classmethod def derive(cls, base: type, name: str = "", /, **ns) -> type[Self]: return type(name, (cls, base), ns) @classmethod def derive_backend(cls, base: type, name: str = "", /, **ns) -> type[Self]: return type(name, (base, cls), ns)
[docs] class P115ID(P115DictAttrLike, int): """整数 id 的封装 """ def __str__(self, /) -> str: return int.__repr__(self)
[docs] class P115StrID(P115DictAttrLike, str): """字符串 id 的封装 """
[docs] class P115URL(P115DictAttrLike, str): """下载链接的封装 """ def geturl(self, /) -> str: return str(self) url = property(geturl)
[docs] class TaskResultTuple(NamedTuple): """任务的执行结果 - indeed: 是否实际执行并成功完成了任务 - 如果为 True,表示成功完成了任务,此时 `error` 必为 None - 如果为 False,则分两种情况讨论 1. `error` 为 None,则表示可能已经成功执行过此任务 2. `error` 为异常实例,就是发生了错误导致执行失败(但任务的产出未必会被清理,例如中断的下载文件可能不会会被保留以待断点续传) - error: 被捕获的异常,如果为 None,则没有发生异常 """ indeed: bool = True error: None | BaseException = None def __bool__(self, /) -> bool: return self.indeed