From 83d789203981dc06603b129ae6ddc241fcd27b56 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 11 May 2024 00:21:14 +0000 Subject: [PATCH] meta: add files --- .gitignore | 5 + README.md | 79 ++++++ UNLICENCE | 24 ++ devbox.json | 7 + devbox.lock | 67 +++++ lfcircle.py | 715 +++++++++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 482 +++++++++++++++++++++++++++++++++ pyproject.toml | 27 ++ 8 files changed, 1406 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 UNLICENCE create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 lfcircle.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d48879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +.devbox/ +.mypy_cache/ +.venv_cache/ +.venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5421786 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# lfcircle + +last.fm statistics generator for your friend circle! + +- [users' guide](#users-guide) + - [installation](#installation) + - [command-line usage](#command-line-usage) + - [example usage](#example-usage) + +## users' guide + +### installation + +> [!IMPORTANT] +> you should probably use pipx to install lfcircle. +> [read about installing it here if you don't have it already!](https://github.com/pypa/pipx?tab=readme-ov-file#install-pipx) + +install lfcircle via pipx + +```text +pipx install git+https://github.com/markjoshwel/lfcircle +``` + +### command-line usage + +```text +usage: lfcircle [-h] [-H HEADER] [-t] [-l] [-a] [-f {ascii,markdown}] + [-v] [targets ...] + +last.fm statistics generator for your friend circle! + +positional arguments: + targets users to target + +options: + -h, --help show this help message and exit + -H HEADER, --header HEADER + specify a report header, leave empty for none + -t, --truncate-scheme + removes 'https://www.' in any links + -l, --lowercase makes everything lowercase + -a, --all-the-links adds links for top artists, albums and tracks + -f {ascii,telegram}, --format {ascii,telegram} + output format type + -v, --verbose enable verbose logging +``` + +### example usage + +```text +$ lfcircle user1 user2 user3 --header "woah statistics" --format ascii --lowercase + +woah statistics +--------------- + +1. user2 — Σ109h; 293s/d + + + 2053 scrobbles (#1) + 588 artists (#1) : 椎名林檎 + 826 albums (#1) : elijah fox — wyoming (piano works) + 1128 tracks (#1) : kero kero bonito — cinema + +2. user3 — Σ41h; 91s/d + + + 640 scrobbles (#2) + 146 artists (#3) : louis cole + 262 albums (#2) : mimideath — effective. power + 379 tracks (#2) : aphex twin — syro u473t8+e [141.98][piezoluminescence mix] + +3. user1 — Σ29h; 66s/d + + + 462 scrobbles (#3) + 159 artists (#2) : seraphine noir + 213 albums (#3) : joywave — how do you feel now? + 261 tracks (#3) : mili — gertrauda +``` diff --git a/UNLICENCE b/UNLICENCE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENCE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..ac986ab --- /dev/null +++ b/devbox.json @@ -0,0 +1,7 @@ +{ + "packages": [ + "python312@latest", + "poetry@latest", + "ruff@latest" + ] +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..b73997c --- /dev/null +++ b/devbox.lock @@ -0,0 +1,67 @@ +{ + "lockfile_version": "1", + "packages": { + "poetry@latest": { + "last_modified": "2024-05-05T12:27:12Z", + "plugin_version": "0.0.4", + "resolved": "github:NixOS/nixpkgs/ee4a6e0f566fe5ec79968c57a9c2c3c25f2cf41d#poetry", + "source": "devbox-search", + "version": "1.8.2", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/f2ccsamq9ihl22qdq1csxqq2j33i2qzm-python3.11-poetry-1.8.2" + }, + "aarch64-linux": { + "store_path": "/nix/store/iv04hlc8xw5br8hy55zzahp779akd7di-python3.11-poetry-1.8.2" + }, + "x86_64-darwin": { + "store_path": "/nix/store/gs5vyiag9w6g4ypyzy8r4ld3r004pxdz-python3.11-poetry-1.8.2" + }, + "x86_64-linux": { + "store_path": "/nix/store/91hqj12jj1249hymnvb0sd6qzm5fhz32-python3.11-poetry-1.8.2" + } + } + }, + "python312@latest": { + "last_modified": "2024-05-03T15:42:32Z", + "plugin_version": "0.0.3", + "resolved": "github:NixOS/nixpkgs/5fd8536a9a5932d4ae8de52b7dc08d92041237fc#python312", + "source": "devbox-search", + "version": "3.12.3", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/7bz0d53vngyxzhs6axavfw2dhl9vwlww-python3-3.12.3" + }, + "aarch64-linux": { + "store_path": "/nix/store/nw7n1zgfiwxvrlxcn6nkggvxvk6msvl9-python3-3.12.3" + }, + "x86_64-darwin": { + "store_path": "/nix/store/g8jihlbhc9sv3d2i4lnagyn2mj74mmjf-python3-3.12.3" + }, + "x86_64-linux": { + "store_path": "/nix/store/aav6i2sih47qjwz0shpl4mmpp877v16k-python3-3.12.3" + } + } + }, + "ruff@latest": { + "last_modified": "2024-05-06T06:27:37Z", + "resolved": "github:NixOS/nixpkgs/dd1290b0f857782a60b251f89651c831cd3eef9d#ruff", + "source": "devbox-search", + "version": "0.4.3", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/9swiy4y7d75jgryswh1hhc60mpki0sdl-ruff-0.4.3" + }, + "aarch64-linux": { + "store_path": "/nix/store/qphi7qrnaig302fhglbkyjygaiy47d10-ruff-0.4.3" + }, + "x86_64-darwin": { + "store_path": "/nix/store/p1jpghv0zyqjvf43hxvj4zcrin9z5ic9-ruff-0.4.3" + }, + "x86_64-linux": { + "store_path": "/nix/store/1fd8kdlbsmgcq60z7pnvwzh7rf6ggqh1-ruff-0.4.3" + } + } + } + } +} diff --git a/lfcircle.py b/lfcircle.py new file mode 100644 index 0000000..3726a11 --- /dev/null +++ b/lfcircle.py @@ -0,0 +1,715 @@ +""" +lfcircle: last.fm statistics generator for your friend circle! +-------------------------------------------------------------- +with all my heart, from me to you +mark , 2024 + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to +""" + +from argparse import ArgumentParser +from datetime import datetime, timedelta +from enum import Enum +from functools import wraps +from sys import stderr +from textwrap import indent +from time import sleep +from traceback import format_exception +from typing import Callable, NamedTuple, ParamSpec, TypeVar +from urllib.parse import unquote + +from bs4 import BeautifulSoup +from requests import Response +from requests import get as _get + +USER_AGENT = ( + "Mozilla/5.0 (compatible; lfcircle; https://github.com/markjoshwel/lfcircle)" +) + + +class FormatTypeEnum(Enum): + """ + enum for what kind of formatting the results are to be shown in + + - `ASCII`: readable ascii that could also work as markdown + - `TELEGRAM`: a weird amalgam of markdown and plaintext + """ + + ASCII = "ascii" + TELEGRAM = "telegram" + + +class Behaviour(NamedTuple): + """ + data structure dictating the operation of lfcircle + + - `targets: list[str] = []` \\ + users to target + + - `header: str = ""` \\ + specify a report header, leave empty for none + + - `truncate_scheme: bool = False` \\ + removes 'https://' in any links + + - `lowercase: bool = False` \\ + makes everything lowercase + + - `all_the_links: bool = False` \\ + adds links for top artists, albums and tracks + + - `format: FormatTypeEnum = FormatTypeEnum.ASCII` \\ + what format to output, see FormatTypeEnum + + - `verbose: bool = False` \\ + enable verbose logging + """ + + targets: list[str] = [] + header: str = "" + truncate_scheme: bool = False + lowercase: bool = False + all_the_links: bool = False + format: FormatTypeEnum = FormatTypeEnum.ASCII + verbose: bool = False + + +def handle_args() -> Behaviour: + """helper function to handle cli args""" + info = __doc__.strip().split("\n", maxsplit=1)[0].split(":", maxsplit=1) + default_behaviour = Behaviour() + + parser = ArgumentParser( + prog=info[0].strip(), + description=info[-1].strip(), + ) + + parser.add_argument( + "targets", + nargs="*", + type=str, + help="users to target", + ) + parser.add_argument( + "-H", + "--header", + type=str, + help="specify a report header, leave empty for none", + default=default_behaviour.header, + ) + parser.add_argument( + "-t", + "--truncate-scheme", + action="store_true", + help="removes 'https://www.' in any links", + default=default_behaviour.truncate_scheme, + ) + parser.add_argument( + "-l", + "--lowercase", + action="store_true", + help="makes everything lowercase", + default=default_behaviour.lowercase, + ) + parser.add_argument( + "-a", + "--all-the-links", + action="store_true", + help="adds links for top artists, albums and tracks", + default=default_behaviour.all_the_links, + ) + parser.add_argument( + "-f", + "--format", + type=str, + help="output format type", + choices=[v.value for v in FormatTypeEnum], + default=default_behaviour.format, + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="enable verbose logging", + ) + + args = parser.parse_args() + return Behaviour( + targets=args.targets, + header=args.header, + truncate_scheme=args.truncate_scheme, + lowercase=args.lowercase, + all_the_links=args.all_the_links, + format=FormatTypeEnum(args.format), + verbose=args.verbose, + ) + + +P = ParamSpec("P") +R = TypeVar("R") + + +class Limiter: + """helper to class to not bomb last.hq""" + + max_per_second: int = 1 + user_agent: str = USER_AGENT + last_call: datetime | None = None + + def limit( + self, func: Callable[P, R], sleeper: Callable[[float], None] = sleep + ) -> Callable[P, R]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs): + if self.last_call is None: + self.last_call = datetime.now() + return func(*args, **kwargs) + + while (self.last_call + timedelta(seconds=1)) > (now := datetime.now()): + sleeper(1) + + self.last_call = now + return func(*args, **kwargs) + + return wrapper + + +class ThingWithScrobbles(NamedTuple): + """shared data structure for artists, albums and tracks""" + + name: str = "" + scrobbles: int = 0 + url: str | None = None + + +def _qualified_thing_name(thing: ThingWithScrobbles) -> str: + """use the url of a 'thing' to get a more qualified name""" + + if thing.url is None: + return thing.name + + right = thing.name + left = ( + unquote(thing.url) + .lstrip("https://www.last.fm/music/") + .replace("+", " ") + .split("/", maxsplit=1) + ) + + return right if (len(left) == 0) else f"{left[0]} — {right}" + + +class ListeningReport(NamedTuple): + """data structure representing a last.fm listening report""" + + user: str + url: str + scrobbles_count: int + scrobbles_daily_avg: int + artists_count: int + albums_count: int + tracks_count: int + artists: tuple[ThingWithScrobbles, ...] + albums: tuple[ThingWithScrobbles, ...] + tracks: tuple[ThingWithScrobbles, ...] + artists_top_new: ThingWithScrobbles + albums_top_new: ThingWithScrobbles + tracks_top_new: ThingWithScrobbles + listening_time_hours: int + + def to_str( + self, + behaviour: Behaviour, + leaderboard_pos: int, + leaderboard_scrobble_pos: int, + leaderboard_artists_pos: int, + leaderboard_albums_pos: int, + leaderboard_tracks_pos: int, + leaderboard_n: int, + ) -> str: + text: str = "" + + match behaviour.format: + case FormatTypeEnum.ASCII: + basket: list[str] = [] + + # intro + basket.append( + (_prefix := f"{leaderboard_pos}. ") + + f"{self.user} — Σ{self.listening_time_hours}h; {self.scrobbles_daily_avg}s/d " + ) + basket.append( + indent(f"<{self.url}>", prefix=(prefix := " " * len(_prefix))) + + "\n" + ) + + rmax = len(f"#{leaderboard_n}") + + # detail 1: total period scrobble count + d1_l = indent(ls := f"{self.scrobbles_count} scrobbles", prefix=prefix) + d1_r = " (" + f"#{leaderboard_scrobble_pos}".rjust(rmax) + ")" + basket.append(d1_l + d1_r) + + # detail 2: total period artist count + d2_l = indent( + f"{self.artists_count} artists".ljust(len(ls)) + + " (" + + f"#{leaderboard_artists_pos}".rjust(rmax) + + ") : ", + prefix=prefix, + ) + d2_r = self.artists[0].name + d2_url = ( + ("\n" + indent(f"<{self.artists[0].url}>", prefix=" " * len(d2_l))) + if behaviour.all_the_links + else "" + ) + basket.append(d2_l + d2_r + d2_url) + + # detail 3: total period album count + d3_l = indent( + f"{self.albums_count} albums".ljust(len(ls)) + + " (" + + f"#{leaderboard_albums_pos}".rjust(rmax) + + ") : ", + prefix=prefix, + ) + d3_r = _qualified_thing_name(self.albums[0]) + d3_url = ( + ("\n" + indent(f"<{self.albums[0].url}>", prefix=" " * len(d2_l))) + if behaviour.all_the_links + else "" + ) + basket.append(d3_l + d3_r + d3_url) + + # detail 4: total period tracks count + d4_l = indent( + f"{self.artists_count} tracks".ljust(len(ls)) + + " (" + + f"#{leaderboard_tracks_pos}".rjust(rmax) + + ") : ", + prefix=prefix, + ) + d4_r = _qualified_thing_name(self.tracks[0]) + d4_url = ( + ("\n" + indent(f"<{self.tracks[0].url}>", prefix=" " * len(d2_l))) + if behaviour.all_the_links + else "" + ) + basket.append(d4_l + d4_r + d4_url) + + if not behaviour.lowercase: + text = "\n".join(basket) + + else: + text = "\n".join(basket[:3] + [s.lower() for s in basket[3:]]) + + case FormatTypeEnum.TELEGRAM: + basket: list[str] = [] + prefix: str = " " + + # intro + basket.append( + f"{leaderboard_pos}. [{self.user}]({self.url}) " + f"— Σ{self.listening_time_hours}h; {self.scrobbles_daily_avg}s/d " + ) + + # detail 1: total period scrobble count + basket.append(f"{prefix}**{self.scrobbles_count} scrobbles**") + + # detail 2: total period artist count + basket.append( + f"{prefix}{self.artists_count} artists (#{leaderboard_artists_pos}): " + + ( + f"[{self.artists[0].name}]({self.artists[0].url})" + if behaviour.all_the_links + else self.artists[0].name + ) + ) + + # detail 3: total period album count + _qual_album_name = _qualified_thing_name(self.albums[0]) + basket.append( + f"{prefix}{self.albums_count} albums (#{leaderboard_albums_pos}): " + + ( + f"[{_qual_album_name}]({self.albums[0].url})" + if behaviour.all_the_links + else _qual_album_name + ) + ) + + # detail 4: total period tracks count + _qual_track_name = _qualified_thing_name(self.tracks[0]) + basket.append( + f"{prefix}{self.tracks_count} tracks (#{leaderboard_tracks_pos}): " + + ( + f"[{_qual_track_name}]({self.tracks[0].url})" + if behaviour.all_the_links + else _qual_track_name + ) + ) + + if not behaviour.lowercase: + text = "\n".join(basket) + + else: + text = "\n".join(basket[:1] + [s.lower() for s in basket[1:]]) + + case _: + raise NotImplementedError( + f"unexpected behaviour format '{behaviour.format}'" + ) + + if behaviour.truncate_scheme: + text = text.replace("https://www.", "") + + return text + + +def get_listening_report( + target: str, + limiter: Limiter, + behaviour: Behaviour, +) -> ListeningReport: + target_url: str = f"https://www.last.fm/user/{target}/listening-report/week" + page_res: Response = limiter.limit(_get)(target_url) + + if page_res.status_code != 200: + raise Exception( + f"non-nominal status code {page_res.status_code} for '{target_url}'" + ) + + page = BeautifulSoup(page_res.text, "html5lib") + + return ListeningReport( + user=target, + url=target_url, + scrobbles_count=_get_scrobbles_count(page), + scrobbles_daily_avg=_get_scrobbles_daily_avg(page), + artists_count=_get_artists_count(page), + albums_count=_get_albums_count(page), + tracks_count=_get_tracks_count(page), + artists=_get_artists(page), + albums=_get_albums(page), + tracks=_get_tracks(page), + artists_top_new=_get_artists_top_new(page), + albums_top_new=_get_albums_top_new(page), + tracks_top_new=_get_tracks_top_new(page), + listening_time_hours=_get_listening_time_hours(page), + ) + + +def _get_scrobbles_count(page: BeautifulSoup) -> int: + assert (_1 := page.select_one(".report-headline-total")) is not None + return int(_1.text.strip().replace(",", "")) + + +def _get_scrobbles_daily_avg(page: BeautifulSoup) -> int: + needle: str = "Average scrobbles" + for fact in (facts := page.select(".report-box-container--quick-fact")): + if needle not in fact.text: + continue + + assert (_1 := fact.select_one(".quick-fact-data-value")) is not None + return int(_1.text.strip().replace(",", "")) + + else: + raise Exception(f"could not find '{needle}' fact, {len(facts)=}") + + +def _get_listening_time_hours(page: BeautifulSoup) -> int: + needle: str = "Listening time" + for fact in (facts := page.select(".report-box-container--quick-fact")): + if needle not in fact.text: + continue + + assert (_d1 := fact.select_one(".quick-fact-data-value")) is not None + days: int = int(_d1.text.strip().replace(",", "")) + + assert (_h1 := fact.select_one(".quick-fact-data-detail")) is not None + hours: int = int(_h1.text.strip().lstrip("days,").rstrip("hours").strip()) + + return (days * 24) + hours + + else: + raise Exception(f"could not find '{needle}' fact, {len(facts)=}") + + +def _get_overview_scrobbles(page: BeautifulSoup, needle: str) -> int: + assert (_1 := page.select_one(needle)) is not None + assert (_2 := _1.select_one(".top-item-overview__scrobbles")) is not None + return int(_2.text.strip().replace(",", "")) + + +def _get_artists_count(page: BeautifulSoup) -> int: + return _get_overview_scrobbles(page=page, needle=".top-item-overview--artist") + + +def _get_albums_count(page: BeautifulSoup) -> int: + return _get_overview_scrobbles(page=page, needle=".top-item-overview--album") + + +def _get_tracks_count(page: BeautifulSoup) -> int: + return _get_overview_scrobbles(page=page, needle=".top-item-overview--track") + + +def _get_top_overview( + page: BeautifulSoup, + top_id: str, + view_needle: str, + select_needle: str, +) -> tuple[ThingWithScrobbles, ...]: + things: list[ThingWithScrobbles] = [] + + # top + assert (_11 := page.select_one(top_id)) is not None + assert (_12 := _11.select_one(".top-item-modal-header")) is not None + assert (_13 := _11.select_one(".top-item-modal-data-item-value")) is not None + assert (_14 := _11.select_one(".top-item-modal-link-text")) is not None + assert view_needle in _14.text, "they moved the damn button" + + things.append( + ThingWithScrobbles( + name=_12.text.strip(), + scrobbles=int(_13.text.strip().replace(",", "")), + url=f"https://www.last.fm{_14.attrs.get('href', '/')}", + ) + ) + + # the rest + for top in page.select(".listening-report-row__col--top-items"): + if len(top.select(select_needle)) == 0: + continue + + assert ( + _n := top.select(".listening-report-secondary-top-item-name") + ) is not None + assert ( + _v := top.select(".listening-report-secondary-top-item-value") + ) is not None + assert len(_n) == len(_v) + + for n, v in zip( + [x.text.strip() for x in _n], + [int(y.text.strip()) for y in _v], + ): + things.append(ThingWithScrobbles(name=n, scrobbles=v)) + + return tuple(things) + + else: + raise Exception(f"could not find '{select_needle}' top overview") + + +def _get_artists(page: BeautifulSoup) -> tuple[ThingWithScrobbles, ...]: + return _get_top_overview( + page=page, + top_id="#top-artist", + view_needle="View Artist page", + select_needle=".top-item-overview--artist", + ) + + +def _get_albums(page: BeautifulSoup) -> tuple[ThingWithScrobbles, ...]: + return _get_top_overview( + page=page, + top_id="#top-album", + view_needle="View Album page", + select_needle=".top-item-overview--album", + ) + + +def _get_tracks(page: BeautifulSoup) -> tuple[ThingWithScrobbles, ...]: + return _get_top_overview( + page=page, + top_id="#top-track", + view_needle="View Track page", + select_needle=".top-item-overview--track", + ) + + +def _get_top_new_thing(page: BeautifulSoup, select_needle: str) -> ThingWithScrobbles: + for top in page.select(".listening-report-row__col--top-items"): + if len(top.select(select_needle)) == 0: + continue + + assert (_t := top.select_one(".top-new-item-title")) is not None + assert (_c := top.select_one(".top-new-item-count")) is not None + + name: str = _t.text.strip() + scrobbles: str = _c.text.replace("scrobbles", "").replace(",", "").strip() + + return ThingWithScrobbles( + name=name if scrobbles.isnumeric() else "", + scrobbles=int(scrobbles) if scrobbles.isnumeric() else 0, + ) + + else: + raise Exception(f"could not find '{select_needle}' top overview") + + +def _get_artists_top_new(page: BeautifulSoup) -> ThingWithScrobbles: + return _get_top_new_thing(page=page, select_needle=".top-new-item-type__artist") + + +def _get_albums_top_new(page: BeautifulSoup) -> ThingWithScrobbles: + return _get_top_new_thing(page=page, select_needle=".top-new-item-type__album") + + +def _get_tracks_top_new(page: BeautifulSoup) -> ThingWithScrobbles: + return _get_top_new_thing(page=page, select_needle=".top-new-item-type__track") + + +def _sorter(r: ListeningReport) -> int: + return r.listening_time_hours + r.scrobbles_count + + +def make_circle_report( + listening_reports: list[ListeningReport], + behaviour: Behaviour, +) -> str: + text: list[str] = [] + + if behaviour.header != "": + match behaviour.format: + case FormatTypeEnum.ASCII: + text.append(behaviour.header) + text.append(("-" * len(behaviour.header))) + text.append("") + + case FormatTypeEnum.TELEGRAM: + text.append(behaviour.header + "\n") + + for leaderboard_pos, report in enumerate( + reversed(sorted(listening_reports, key=_sorter)), + start=1, + ): + leaderboard_scrobble_pos: int = 0 + for leaderboard_scrobble_pos, _report in enumerate( + reversed( + sorted( + listening_reports, + key=lambda r: r.scrobbles_count, + ) + ), + start=1, + ): + if report == _report: + break + + leaderboard_artists_pos: int = 0 + for leaderboard_artists_pos, _report in enumerate( + reversed( + sorted( + listening_reports, + key=lambda r: r.artists_count, + ) + ), + start=1, + ): + if report == _report: + break + + leaderboard_albums_pos: int = 0 + for leaderboard_albums_pos, _report in enumerate( + reversed( + sorted( + listening_reports, + key=lambda r: r.albums_count, + ) + ), + start=1, + ): + if report == _report: + break + + leaderboard_tracks_pos: int = 0 + for leaderboard_tracks_pos, _report in enumerate( + reversed( + sorted( + listening_reports, + key=lambda r: r.tracks_count, + ) + ), + start=1, + ): + if report == _report: + break + + text.append( + report.to_str( + behaviour=behaviour, + leaderboard_pos=leaderboard_pos, + leaderboard_scrobble_pos=leaderboard_scrobble_pos, + leaderboard_artists_pos=leaderboard_artists_pos, + leaderboard_albums_pos=leaderboard_albums_pos, + leaderboard_tracks_pos=leaderboard_tracks_pos, + leaderboard_n=len(listening_reports), + ) + + "\n" + ) + + return "\n".join(text) + + +def cli() -> None: + behaviour = handle_args() + limiter = Limiter() + reports: list[ListeningReport] = [] + + print(behaviour, file=stderr) if behaviour.verbose else ... + for i, target in enumerate(behaviour.targets): + try: + reports.append( + get_listening_report( + target=target, + behaviour=behaviour, + limiter=limiter, + ) + ) + + except Exception as err: + print( + f"error: skipping target '{target}'\n" + + indent( + "".join(format_exception(type(err), err, err.__traceback__)), + prefix="\t", + ) + ) + + else: + print( + f"{i + 1}/{len(behaviour.targets)}", + file=stderr, + end="\r", + ) + print(reports[-1], file=stderr) if behaviour.verbose else ... + + print(make_circle_report(listening_reports=reports, behaviour=behaviour)) + + +if __name__ == "__main__": + cli() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..624b7eb --- /dev/null +++ b/poetry.lock @@ -0,0 +1,482 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "mypy" +version = "1.10.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20240504" +description = "Typing stubs for beautifulsoup4" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-beautifulsoup4-4.12.0.20240504.tar.gz", hash = "sha256:d7b7af4ccc52fc22784d33a529695e34329a9bdd5db6a649c9b25cb2c3a148d5"}, + {file = "types_beautifulsoup4-4.12.0.20240504-py3-none-any.whl", hash = "sha256:84e04e4473b3c79da04d9d1f89d20a38e5cc92f9c080a8e1f9d36a287b350465"}, +] + +[package.dependencies] +types-html5lib = "*" + +[[package]] +name = "types-html5lib" +version = "1.1.11.20240228" +description = "Typing stubs for html5lib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-html5lib-1.1.11.20240228.tar.gz", hash = "sha256:22736b7299e605ec4ba539d48691e905fd0c61c3ea610acc59922232dc84cede"}, + {file = "types_html5lib-1.1.11.20240228-py3-none-any.whl", hash = "sha256:af5de0125cb0fe5667543b158db83849b22e25c0e36c9149836b095548bf1020"}, +] + +[[package]] +name = "types-requests" +version = "2.31.0.20240406" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, + {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "ae2682e3997241ab5c39d1e2708818aac1d9e2c640219e4bbe53cfde7de769f7" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f0f8b2e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +name = "lfcircle" +version = "0.1.0" +description = "last.fm statistics generator for your friend circle!" +authors = ["Mark Joshwel "] +license = "Unlicense" +readme = "README.md" + +[tool.poetry.scripts] +lfcircle = 'lfcircle:cli' + +[tool.poetry.dependencies] +python = "^3.10" +beautifulsoup4 = "^4.12.3" +requests = "^2.31.0" +html5lib = "^1.1" + +[tool.poetry.group.dev.dependencies] +types-beautifulsoup4 = "^4.12.0.20240504" +mypy = "^1.10.0" +isort = "^5.13.2" +black = "^24.4.2" +types-requests = "^2.31.0.20240406" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"