from__future__importannotationsimportrefromcollections.abcimportCallablefromtypingimportGeneric,NamedTuple,TypeVar,overloadfromlxmlimporthtmlfrom.errorsimportInvalidURLErrorT=TypeVar("T")T_co=TypeVar("T_co",covariant=True)AO3_STORY_REGEX=re.compile(r"(?:https://|)(?:www\.|)archiveofourown\.org/(?:works|series)/(?P<id>\d+)")ICON_URL_USER_ID_REGEX=re.compile(r".*/(\d+)/")__all__=("Constraint","get_id_from_url",)AO3_LOGO_URL="https://archiveofourown.org/images/ao3_logos/logo.png"classCachedSlotProperty(Generic[T,T_co]):"""An implementation of a cached property for slotted classes. Copied from discord.py: https://github.com/Rapptz/discord.py/blob/master/discord/utils.py#L208. Credit goes to Rapptz and contributors. """def__init__(self,name:str,function:Callable[[T],T_co])->None:self.name=nameself.function=functionself.__doc__=function.__doc__@overloaddef__get__(self,instance:T,owner:type[T]|None=...)->T_co:...@overloaddef__get__(self,instance:None,owner:type[T]|None=...)->CachedSlotProperty[T,T_co]:...def__get__(self,instance:T|None,owner:type[T]|None=None)->T_co|CachedSlotProperty[T,T_co]:ifinstanceisNone:returnselftry:returngetattr(instance,self.name)exceptAttributeError:value=self.function(instance)setattr(instance,self.name,value)returnvaluedefcached_slot_property(name:str)->Callable[[Callable[[T],T_co]],CachedSlotProperty[T,T_co]]:defdecorator(func:Callable[[T],T_co])->CachedSlotProperty[T,T_co]:returnCachedSlotProperty(name,func)returndecorator
[docs]classConstraint(NamedTuple):"""A representation for a constraint on integer amounts via a range. Attributes ---------- min_val: :class:`int`, default=0 The lower end of the constraint. Defaults to 0. max_val: :class:`int` | None, optional The upper bound of the constraint. Defaults to None. """min_val:int=0max_val:int|None=None@propertydefstring(self)->str:"""Stringify the constraint in a way that AO3 can understand."""ifself.min_val==0andself.max_valisNone:return""ifself.min_val==0:returnf"<{self.max_val}"ifself.max_valisNone:returnf">{self.min_val}"ifself.min_val==self.max_val:returnstr(self.min_val)returnf"{self.min_val}-{self.max_val}"
[docs]defget_id_from_url(url:str,*,will_raise:bool=False)->int|None:"""Get the work/series ID from an AO3 website url. Parameters ---------- url: :class:`str` The AO3 url. Could be for a series or a work. will_raise: :class:`bool`, optional Whether to raise an exception if an id is not found in the given string. Defaults to False. Returns ------- :class:`int` | None The work/series ID, or None if not found. Raises ------ InvalidURLError The given URL doesn't match the expected AO3 work/series URL structure. """result=AO3_STORY_REGEX.search(url)ifresult:returnint(result.group("id"))ifnotwill_raise:returnNoneraiseInvalidURLError
defparse_max_pages_num(element:html.HtmlElement)->int:default_page_num=1try:num_gen=(int(li.text_content().strip())forliinelement.cssselect("ol[title=pagination] li"))returnmax(num_gen)exceptAttributeError:returndefault_page_numdefextract_login_auth_token(text:str|html.HtmlElement)->str|None:element=html.fromstring(text)ifisinstance(text,str)elsetexttry:returnelement.cssselect("input[name=authenticity_token]")[0].get("value",None)exceptIndexError:returnNonedefextract_csrf_token(text:str|html.HtmlElement)->str|None:element=html.fromstring(text)ifisinstance(text,str)elsetexttry:returnelement.cssselect("meta[name=csrf-token]")[0].get("content",None)exceptIndexError:returnNonedefextract_pseud_id(element:html.HtmlElement,specified_pseud:str|None=None)->str|None:pseuds=element.cssselect('input[name$="[pseud_id]"]')iflen(pseuds)>0:returnpseuds[0].get("value")pseuds=element.cssselect('select[name$="[pseud_id]"]')iflen(pseuds)>0:returnnext((option.get("value")foroptioninpseuds[0].cssselect("option")ifbool((str(option.text_content())==specified_pseud)ifspecified_pseudelseoption.get("selected"),)),None,)returnNonedefint_or_none(data:str|None)->int|None:"""Remove commas from a string and attempt conversion to an int. If anything fails along the way, return None."""ifdataisNone:returnNonetry:returnint(data.replace(",",""))exceptValueError:returnNone