PK!U . .polka.py"""A module provides access to the public polka.academy API.bo""" import json import re from html import unescape from typing import NamedTuple, Optional from urllib.parse import urlencode from urllib.request import urlopen __all__ = [ "rawbooks", "rawbook", "rawsearch", "rawlists", "rawlist", "rawpundits", "rawpunditposts", "rawpunditfavs", "books", "pundits", "lists", "search", "Book", "Pundit", "Compilation", ] _BASE = "https://api.polka.academy/" _BOOKS = f"{_BASE}books?" _POST = f"{_BASE}posts/{{post_id}}" _SEARCH = f"{_BASE}search?" _LISTS = f"{_BASE}compilations" _LIST_POST = f"{_BASE}compilations/{{post_id}}" _PEOPLE = f"{_BASE}people?" _PEOPLE_POST = f"{_BASE}people/{{post_id}}/posts" _PEOPLE_FAVS = f"{_BASE}people/{{post_id}}/favs" _NOTES = re.compile(r"\{([^\|]*)\s*\|\s*([^\}]*)\}") _SOURCES = re.compile(r"\[([^\|]*)\s*\|\s*(\d+)\s*\|([^\]]*)\]") _HTMLTAG = re.compile(r"<\s*[^>]*>") def __dir__(): return sorted(__all__) def _get(url, **params): response = urlopen(url + urlencode(params)) response = response.read().decode("utf-8") return json.loads(response) def _clean_text(text): text = re.sub(_HTMLTAG, "", text) text = unescape(text) text = text.replace("\xa0", " ") text = text.strip() return text def _importance(): return {b["id"]: b["importance"] for b in rawbooks()["books"]} def rawbooks(sort_column="rating", sort_direction="desc"): params = {"sort_column": sort_column, "sort_direction": sort_direction} return _get(_BOOKS, **params) def rawbook(book_id): return _get(_POST.format(post_id=book_id)) def rawsearch(query): return _get(_SEARCH, **{"q": query}) def rawlists(): return _get(_LISTS) def rawlist(list_id): return _get(_LIST_POST.format(post_id=list_id)) def rawpundits(type_="all"): return _get(_PEOPLE, **{"type": type_}) def rawpunditposts(pundit_id): return _get(_PEOPLE_POST.format(post_id=pundit_id)) def rawpunditfavs(pundit_id): return _get(_PEOPLE_FAVS.format(post_id=pundit_id)) def books(sort_column="rating", sort_direction="desc"): """Returns a list of `Book` instances that has an article. Valid values for `sort_column` are "rating" (default), "year", "title" and "authors". Valid values for `sort_direction` are "desc" (default) and "asc".""" books = [] for data in rawbooks(sort_column, sort_direction)["books"]: books.append(Book(data["id"], rawdata=data)) return books def pundits(type_="all"): """Returns a list of `Pundit` instances. Valid values for `type_` are "all" (default), "authors" and "experts".""" pundits = [] for data in rawpundits(type_)["people"]: pundits.append(Pundit(data["id"], rawdata=data)) return pundits def lists(): """Returns a list of `Compilation` instances.""" lists = [] for data in rawlists()["compilations"]: lists.append(Compilation(data["id"], rawdata=data)) return lists def search(query): """Looks for a `query` and returns a list of 3-tuples like `(title, description, object)`.""" results = [] for item in rawsearch(query): category = item["category"] description = _clean_text(item["desc"]) if category == "articles": Object = Book elif category == "lists": Object = Compilation elif category == "experts": Object = Pundit results.append((item["title"], description, Object(item["id"]))) return results class Book: """Represents a book.""" _importance = {} def __init__(self, id: Optional[int], *, rawdata=None): self.id = id self.rawdata = rawdata if rawdata is not None else {} self._n_requests = 0 def _getdata(self, key): if key not in self.rawdata: if key == "author" and "authors" in self.rawdata: key = "authors" elif key == "date_start": if "start_year" in self.rawdata: key = "start_year" elif "year" in self.rawdata: key = "year" else: data = rawbook(self.id) self.rawdata.update(data) self._n_requests += 1 elif key == "date_end": if "end_year" in self.rawdata: key = "start_year" else: data = rawbook(self.id) self.rawdata.update(data) self._n_requests += 1 elif key == "lead" and "description" in self.rawdata: key = "description" elif key == "lead" and not self.has_article and self.in_lists: books = rawlist(self.in_lists[0].id)["books"] data = [d for d in books if d["id"] == self.id][0] self.rawdata.update(data) self._n_requests += 1 key = "description" elif key == "importance" and self.has_article: Book._importance = _importance() self._n_requests += 1 self.rawdata.update({"importance": Book._importance[self.id]}) elif key == "compilations" and self.has_article: books = rawbooks()["books"] data = [b for b in books if b["id"] == self.id][0] self.rawdata.update(data) elif self.has_article: data = rawbook(self.id) self.rawdata.update(data) self._n_requests += 1 return self.rawdata.get(key) @property def importance(self): importance = self._getdata("importance") return float(importance) if importance is not None else None @property def title(self): return _clean_text(self._getdata("title")) @property def authors(self): return self._getdata("author") @property def description(self): lead = self._getdata("lead") return _clean_text(lead) if lead is not None else lead @property def pundit(self): if not self.has_article: return None data = self._getdata("pundit") return Pundit(data["id"], rawdata=data) @property def year(self): start = self._getdata("date_start") end = self._getdata("date_end") return Year(start, end) @property def has_article(self): return self.id is not None @property def questions(self): """Returns a list of `Question` instances (just named tuples). Each item has `question`, `answer` and `answer_with_notes` attributes. """ if not self.has_article: return None questions = [] for block in self._getdata("blocks") or []: if block["type"] != "question_template": continue question = _clean_text(block["question"].strip()) answer_with_notes = _clean_text(block["html"].strip()) answer = re.sub(_NOTES, r"\1", answer_with_notes) answer = re.sub(_SOURCES, r"\1", answer) questions.append(Question(question, answer, answer_with_notes)) return questions @property def sources(self): if not self.has_article: return None sources = self._getdata("list") return [source["title"] for source in sources] @property def in_lists(self): lists = [] for data in self._getdata("compilations"): lists.append(Compilation(data["id"], rawdata=data)) return lists def __repr__(self): return ( f"{self.__class__.__name__}" f"(title={self.title!r}, authors={self.authors!r})" ) def __eq__(self, other): if other.__class__ is self.__class__: return (self.id, self.title) == (other.id, other.title) return NotImplemented def __lt__(self, other): if other.__class__ is self.__class__: return self.importance < other.importance return NotImplemented def __hash__(self): return hash((self.id, self.title)) class Pundit: "Represents an expert." def __init__(self, id: int, *, rawdata=None): self.id = id self.rawdata = rawdata if rawdata is not None else {} self._n_requests = 0 def _getdata(self, key): if key not in self.rawdata: if key == "posts": posts = rawpunditposts(self.id)["books"] self.rawdata.update({"posts": posts}) elif key == "favs": favs = rawpunditfavs(self.id)["books"] self.rawdata.update({"favs": favs}) else: people = rawpundits()["people"] data = [p for p in people if p["id"] == self.id][0] self.rawdata.update(data) self._n_requests += 1 return self.rawdata.get(key) @property def name(self): first = self._getdata("first").strip() last = self._getdata("last").strip() return f"{first} {last}" @property def credit(self): return _clean_text(self._getdata("credit")) @property def description(self): return _clean_text(self._getdata("description")) @property def wrote_about(self): posts = self._getdata("posts") return [Book(d["id"], rawdata=d) for d in posts] if posts else [] @property def favorites(self): favs = self._getdata("favs") return [Book(d["id"], rawdata=d) for d in favs] if favs else [] def __repr__(self): return f"{self.__class__.__name__}(name={self.name!r})" def __eq__(self, other): if other.__class__ is self.__class__: return (self.id, self.name) == (other.id, other.name) return NotImplemented def __hash__(self): return hash((self.id, self.name)) class Compilation: """Represents a compilation""" def __init__(self, id: int, *, rawdata=None): self.id = id self.rawdata = rawdata if rawdata is not None else {} self._n_requests = 0 def _getdata(self, key): if key not in self.rawdata: if key in ["short_desc", "max_year", "min_year"]: compilations = rawlists()["compilations"] data = [c for c in compilations if c["id"] == self.id][0] self.rawdata.update(data) else: self.rawdata.update(rawlist(self.id)) self._n_requests += 1 return self.rawdata.get(key) @property def title(self): return self._getdata("title") @property def description(self): return _clean_text(self._getdata("description")) @property def short_description(self): return _clean_text(self._getdata("short_desc")) @property def max_year(self): return int(self._getdata("max_year")) @property def min_year(self): return int(self._getdata("min_year")) @property def books(self): books = [] for data in self._getdata("books"): books.append(Book(data["id"], rawdata=data)) return books def __repr__(self): return f"{self.__class__.__name__}(title={self.title!r})" def __eq__(self, other): if other.__class__ is self.__class__: return (self.id, self.title) == (other.id, other.title) return NotImplemented def __hash__(self): return hash((self.id, self.title)) class Year(NamedTuple): start: Optional[int] end: Optional[int] class Question(NamedTuple): question: str answer: str answer_with_notes: str PK!HڽTUpolka-2.2.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H]+polka-2.2.0.dist-info/METADATAZnϧ8PBWtIn]G܌(EK+x IYNXr,,rkJ(^?+ fFq\4#r\o#T2Kl!6S'>)gS%..rRڜ3¼m]6\6\ W3/b]x?eƮU*r),dfҩT=Q\M1Wk WR3ܩ:0;c4/%> ٲ k])BL18PV({\|P(_لdF* ?[Ȗą%HG?{[ֻoghMk(&6\|ߜ+*YNcS. WK\.j>.Сy C:^u^WvlƥB|^\,U3%ۼg߰ ŜWJR.'޳3lB>7qЀKKE2xrla1H4w}nvܞ{ܶw׸geo]Ǹ-vGCyUqSu.}w{=l+L>m:S9q$7ŻXf$&Ho-*.Yw 62=S~2_[5$Ck7ezܑ#cN;nb :6$@=̢% }Ebhr%3?p@䄏h>T*n#8yiz%Q]oPgDVG Cz! IVmDz)\/r bv>q0U~nE׭Wz$RcH2wZp}z'5#~N#zktQ9"ijlX + 'oLT6E@c<)x0X+cs'!J2o=F,2anOH3;Ut$갮y7_K{ [o'm>/FM30?!@C2 ݋#p-d#K{܆em b<:@aC<%(C6"؞Cc]&X1hLPɀ*L&}98&8:S`lI* |bK  bk8 Nk6pbq>, [4ґ\FS]il!z_ƭ uE!=Xƙ٭0s'7ސLEz\54<ʳT'l-HZhN QV$ ܐ(@BfOMNH1")ս$jZ8XcM% u!y3 W`Daq@8%~z쳋&HG-/cx!NȡXmdġcy ΰ_ I I#ԀV9#|`9(ʓMf.[*W0 5m~x_6ˋv$<<9rP!RIr ՠ*ݺ:W[S䚤C*,+Џ82 p))ꁚ6BG2 CmcWݡSvfmrRT?O * "6GxuŴ^NȬ*Y4̪` "zcgOәlG KB=u\ 1~Z֔ #bHbYB{_pVPX敺]-azg$[f0݅T،Z1.Kөj0+u9]At:U)"0?q[0[L'Pr!d]G8TL-"Gqd@'XeۘOS浺P]CюAfMU cW2 V|pJZ61*&B:Bps-fH $*`Uȁ7n̶WJ 6k,J͇ARӌǖX%PsT#x WTMCHꇾb,X2gba $E:"e-@ lAT`pcd!9>pp_L4/ xZ#$m' kOqFR'9|U1MjR֤v,?] Lfj*k }LGb}H>'VQ}$S9d1PapJ6H<1RM@laj . I?Ec -*k^49l VT& "MΟ E#@'w%R[o5!Ұ5G:/hW)D~ R|Ax C2/ eH=;@T/_,Qz9sPIt cm+(=;B=lWUn#}ƹ:Hk~LښeK/$RNwg|Q{H6rBH 8b7 WgZykBR~eA ח'lFQڍjou6W|V6\F/ ѹ_q}T>  $:l4L3@!`OD.mjCsnrའٔc[}sR td3~EzdIjDJޙ )0Rnid7ü*KU..Xu3~Ώxmk 4MaPGBkՆŊAC kĆJq7z-󕝟yF6XHVsGHɁ~S;ZG+.ݶc)*iش\aZαkk{ aX;!-mxa=nGD{]:8F+h Tj^-OZi?a& jPj2 zJ %X\QV_%R} iY^v$qFo}lX U z_L<ܩKFGWa43}رѾ=Ӥ9nݘ0a^#r^5ި #Rڊ8LcMI9ơۥ_-|2A-n* ?t ȝ²-H60Z|O ^|7hL.QcK\=~އ(#PH_1g9؞aF(Wlo&-)3鸶l ^ڳ%ױ%bIukAv z)ނ8!5I{ Q p/ѧ/UњL= ~a,SGc'h;JKJemKȕZ(64 y>:sɉv=?O wgO[Į.w+ӗGX *Ɨc _BhF?i:"H /Uȿ+yLm$&\roPF`[}"N ~,v XCŸ:71+$&'y ߣG$+i2]7=ݠ' PK!Hǡ5| polka-2.2.0.dist-info/RECORDuAr0нgq")_J#a"fӷ]o^+%GYe,1|wSuI]!ܳӳqMͿ0l(z z{v-.\Jo.+uaWMCB}5 ٸݍMbP:+|5.>B_PK!U . .polka.pyPK!HڽTUF.polka-2.2.0.dist-info/WHEELPK!H]+.polka-2.2.0.dist-info/METADATAPK!Hǡ5| %@polka-2.2.0.dist-info/RECORDPK(A