meta: add files

This commit is contained in:
Mark Joshwel 2024-05-11 00:21:14 +00:00
commit 83d7892039
8 changed files with 1406 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
__pycache__/
.devbox/
.mypy_cache/
.venv_cache/
.venv/

79
README.md Normal file
View file

@ -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
<https://last.fm/user/user2/listening-report/week>
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
<https://last.fm/user/user2/listening-report/week>
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
<https://last.fm/user/user1/listening-report/week>
462 scrobbles (#3)
159 artists (#2) : seraphine noir
213 albums (#3) : joywave — how do you feel now?
261 tracks (#3) : mili — gertrauda
```

24
UNLICENCE Normal file
View file

@ -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 <http://unlicense.org/>

7
devbox.json Normal file
View file

@ -0,0 +1,7 @@
{
"packages": [
"python312@latest",
"poetry@latest",
"ruff@latest"
]
}

67
devbox.lock Normal file
View file

@ -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"
}
}
}
}
}

715
lfcircle.py Normal file
View file

@ -0,0 +1,715 @@
"""
lfcircle: last.fm statistics generator for your friend circle!
--------------------------------------------------------------
with all my heart, from me to you
mark <mark@joshwel.co>, 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 <http://unlicense.org/>
"""
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()

482
poetry.lock generated Normal file
View file

@ -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"

27
pyproject.toml Normal file
View file

@ -0,0 +1,27 @@
[tool.poetry]
name = "lfcircle"
version = "0.1.0"
description = "last.fm statistics generator for your friend circle!"
authors = ["Mark Joshwel <mark@joshwel.co>"]
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"