meta: add files
This commit is contained in:
commit
83d7892039
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
__pycache__/
|
||||
.devbox/
|
||||
.mypy_cache/
|
||||
.venv_cache/
|
||||
.venv/
|
79
README.md
Normal file
79
README.md
Normal 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
24
UNLICENCE
Normal 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
7
devbox.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"packages": [
|
||||
"python312@latest",
|
||||
"poetry@latest",
|
||||
"ruff@latest"
|
||||
]
|
||||
}
|
67
devbox.lock
Normal file
67
devbox.lock
Normal 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
715
lfcircle.py
Normal 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
482
poetry.lock
generated
Normal 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
27
pyproject.toml
Normal 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"
|
Loading…
Reference in a new issue