Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
07146e1246 | |||
8f0e846e95 | |||
0505b93539 | |||
1888447570 | |||
dfd9e342bb | |||
d3916aa641 | |||
d2169ce784 |
13 changed files with 298 additions and 33 deletions
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
36
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
36
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
@ -0,0 +1,36 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredUrls">
|
||||
<list>
|
||||
<option value="http://0.0.0.0" />
|
||||
<option value="http://127.0.0.1" />
|
||||
<option value="http://activemq.apache.org/schema/" />
|
||||
<option value="http://cxf.apache.org/schemas/" />
|
||||
<option value="http://java.sun.com/" />
|
||||
<option value="http://javafx.com/fxml" />
|
||||
<option value="http://javafx.com/javafx/" />
|
||||
<option value="http://json-schema.org/draft" />
|
||||
<option value="http://localhost" />
|
||||
<option value="http://maven.apache.org/POM/" />
|
||||
<option value="http://maven.apache.org/xsd/" />
|
||||
<option value="http://primefaces.org/ui" />
|
||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||
<option value="http://schemas.xmlsoap.org/" />
|
||||
<option value="http://tiles.apache.org/" />
|
||||
<option value="http://unlicense.org" />
|
||||
<option value="http://www.ibm.com/webservices/xsd" />
|
||||
<option value="http://www.jboss.com/xml/ns/" />
|
||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||
<option value="http://www.springframework.org/schema/" />
|
||||
<option value="http://www.springframework.org/security/tags" />
|
||||
<option value="http://www.springframework.org/tags" />
|
||||
<option value="http://www.thymeleaf.org" />
|
||||
<option value="http://www.w3.org/" />
|
||||
<option value="http://xmlns.jcp.org/" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
12
.idea/lfcircle.iml
generated
Normal file
12
.idea/lfcircle.iml
generated
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Poetry (lfcircle)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Poetry (lfcircle)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (lfcircle)" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/lfcircle.iml" filepath="$PROJECT_DIR$/.idea/lfcircle.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
10
.idea/poetry.xml
generated
Normal file
10
.idea/poetry.xml
generated
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PoetryConfigService">
|
||||
<option name="poetryVirtualenvPaths">
|
||||
<set>
|
||||
<option value="$USER_HOME$/AppData/Local/pypoetry/Cache/virtualenvs/lfcircle-XN0g8hyi-py3.12/Scripts/python.exe" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
6
.idea/ruff.xml
generated
Normal file
6
.idea/ruff.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RuffConfigService">
|
||||
<option name="projectRuffExecutablePath" value="C:\Users\m\AppData\Local\pypoetry\Cache\virtualenvs\lfcircle-XN0g8hyi-py3.12\Scripts\ruff.exe" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
189
lfcircle.py
189
lfcircle.py
|
@ -32,10 +32,12 @@ For more information, please refer to <http://unlicense.org/>
|
|||
|
||||
from argparse import ArgumentParser
|
||||
from bisect import insort
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
from sys import stderr
|
||||
from sys import stderr, argv
|
||||
from sys import exit as sysexit
|
||||
from textwrap import indent
|
||||
from time import sleep
|
||||
from traceback import format_exception
|
||||
|
@ -49,6 +51,9 @@ FORMAT_TELEGRAM_PREFIX: Final[str] = " "
|
|||
USER_AGENT: Final[str] = (
|
||||
"Mozilla/5.0 " "(compatible; lfcircle; https://github.com/markjoshwel/lfcircle)"
|
||||
)
|
||||
__version__: Final[str] = "0.2.1"
|
||||
|
||||
GlobalTagCounter = dict[str, Counter[str]]
|
||||
|
||||
|
||||
class FormatTypeEnum(Enum):
|
||||
|
@ -103,6 +108,10 @@ def handle_args() -> Behaviour:
|
|||
info = __doc__.strip().split("\n", maxsplit=1)[0].split(":", maxsplit=1)
|
||||
default_behaviour = Behaviour()
|
||||
|
||||
if "--version" in argv:
|
||||
print(__version__)
|
||||
sysexit(0)
|
||||
|
||||
parser = ArgumentParser(
|
||||
prog=info[0].strip(),
|
||||
description=info[-1].strip(),
|
||||
|
@ -239,6 +248,7 @@ class ListeningReport(NamedTuple):
|
|||
albums_top_new: ThingWithScrobbles
|
||||
tracks_top_new: ThingWithScrobbles
|
||||
listening_time_hours: int
|
||||
tags: dict[str, tuple[int, ...]]
|
||||
|
||||
def to_str(
|
||||
self,
|
||||
|
@ -249,11 +259,17 @@ class ListeningReport(NamedTuple):
|
|||
leaderboard_albums_pos: int,
|
||||
leaderboard_tracks_pos: int,
|
||||
leaderboard_n: int,
|
||||
global_tag_counter: GlobalTagCounter,
|
||||
) -> str:
|
||||
basket: list[str] = []
|
||||
prefix: str = ""
|
||||
text: str = ""
|
||||
|
||||
tag_counter: Counter[str]
|
||||
tags: list[str]
|
||||
tv: str
|
||||
tc: int | float
|
||||
|
||||
match behaviour.format:
|
||||
case FormatTypeEnum.ASCII:
|
||||
# intro
|
||||
|
@ -262,7 +278,8 @@ class ListeningReport(NamedTuple):
|
|||
+ 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"
|
||||
indent(f"<{self.url}>", prefix=(prefix := " " * len(_prefix)))
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
rmax = len(f"#{leaderboard_n}")
|
||||
|
@ -320,6 +337,49 @@ class ListeningReport(NamedTuple):
|
|||
)
|
||||
basket.append(d4_l + d4_r + d4_url)
|
||||
|
||||
# detail 5: top five tags
|
||||
if len(self.tags) > 0:
|
||||
tag_counter = Counter[str]()
|
||||
|
||||
for tag_name, tag_values in self.tags.items():
|
||||
tag_counter.update(
|
||||
{tag_name: calculate_tag_score(tag_values, tags=self.tags)}
|
||||
)
|
||||
|
||||
tags = []
|
||||
for tn, _tc in tag_counter.most_common(5):
|
||||
tv = ""
|
||||
tc = round(_tc)
|
||||
|
||||
if tn in global_tag_counter:
|
||||
counter = global_tag_counter[tn]
|
||||
|
||||
if len(counter) > 1:
|
||||
_tv = sum(
|
||||
[
|
||||
i if (user == self.user) else 0
|
||||
for i, (user, _) in enumerate(
|
||||
counter.most_common(), start=1
|
||||
)
|
||||
]
|
||||
)
|
||||
tv = f": #{_tv}" if (_tv > 0) else ""
|
||||
|
||||
_flt_ranks = [val for _, val in counter.most_common()]
|
||||
_int_ranks = [round(val) for val in _flt_ranks]
|
||||
|
||||
if (tc in _int_ranks) and (_int_ranks.count(tc) > 1): # type: ignore
|
||||
tc = round(_tc, 2)
|
||||
|
||||
tags.append(f"{tn} ({tc}%{tv})")
|
||||
|
||||
basket.append(
|
||||
indent(
|
||||
("Top tags".ljust(len(d4_l) - 6) + " : " + ", ".join(tags)),
|
||||
prefix=prefix,
|
||||
)
|
||||
)
|
||||
|
||||
if not behaviour.lowercase:
|
||||
text = "\n".join(basket)
|
||||
|
||||
|
@ -340,7 +400,7 @@ class ListeningReport(NamedTuple):
|
|||
|
||||
# detail 2: total period artist count
|
||||
basket.append(
|
||||
f"{prefix}{self.artists_count} artists (#{leaderboard_artists_pos}): "
|
||||
f"\n{prefix}{self.artists_count} artists (#{leaderboard_artists_pos}): "
|
||||
+ (
|
||||
f"[{self.artists[0].name}]({self.artists[0].url})"
|
||||
if behaviour.all_the_links
|
||||
|
@ -370,6 +430,44 @@ class ListeningReport(NamedTuple):
|
|||
)
|
||||
)
|
||||
|
||||
# detail 5: top tags
|
||||
if len(self.tags) > 0:
|
||||
tag_counter = Counter()
|
||||
|
||||
for tag_name, tag_values in self.tags.items():
|
||||
tag_counter.update(
|
||||
{tag_name: calculate_tag_score(tag_values, tags=self.tags)}
|
||||
)
|
||||
|
||||
tags = []
|
||||
for tn, _tc in tag_counter.most_common(5):
|
||||
tv = ""
|
||||
tc = round(_tc)
|
||||
|
||||
if tn in global_tag_counter:
|
||||
counter = global_tag_counter[tn]
|
||||
|
||||
if len(counter) > 1:
|
||||
_tv = sum(
|
||||
[
|
||||
i if (user == self.user) else 0
|
||||
for i, (user, _) in enumerate(
|
||||
counter.most_common(), start=1
|
||||
)
|
||||
]
|
||||
)
|
||||
tv = f": #{_tv}" if (_tv > 0) else ""
|
||||
|
||||
_flt_ranks = [val for _, val in counter.most_common()]
|
||||
_int_ranks = [round(val) for val in _flt_ranks]
|
||||
|
||||
if (tc in _int_ranks) and (_int_ranks.count(tc) > 1): # type: ignore
|
||||
tc = round(_tc, 2)
|
||||
|
||||
tags.append(f"{tn} ({tc}%{tv})")
|
||||
|
||||
basket.append(f"\n{prefix}Top tags" + ": " + ", ".join(tags))
|
||||
|
||||
if not behaviour.lowercase:
|
||||
text = "\n".join(basket)
|
||||
|
||||
|
@ -392,7 +490,6 @@ def get_listening_report(
|
|||
limiter: Limiter,
|
||||
behaviour: Behaviour,
|
||||
) -> ListeningReport:
|
||||
|
||||
target_url: str = f"https://www.last.fm/user/{target}/listening-report/week"
|
||||
|
||||
page_res: Response = limiter.limit(get)(
|
||||
|
@ -422,20 +519,25 @@ def get_listening_report(
|
|||
albums_top_new=_get_albums_top_new(page),
|
||||
tracks_top_new=_get_tracks_top_new(page),
|
||||
listening_time_hours=_get_listening_time_hours(page),
|
||||
tags=_get_tags(page),
|
||||
)
|
||||
|
||||
|
||||
def _int(number: str) -> int:
|
||||
def _int(number: str, failable: bool = False) -> int:
|
||||
n = (
|
||||
number.replace(",", "")
|
||||
.replace("scrobbles", "")
|
||||
.strip()
|
||||
.lstrip("days,")
|
||||
.lstrip("days")
|
||||
.rstrip("hours")
|
||||
.strip()
|
||||
)
|
||||
assert n.isnumeric()
|
||||
return int(n)
|
||||
if failable and n == "":
|
||||
return 0
|
||||
else:
|
||||
assert n.isnumeric()
|
||||
return int(n)
|
||||
|
||||
|
||||
def _get_scrobbles_count(page: BeautifulSoup) -> int:
|
||||
|
@ -466,7 +568,7 @@ def _get_listening_time_hours(page: BeautifulSoup) -> int:
|
|||
days: int = _int(_d1.text)
|
||||
|
||||
assert (_h1 := fact.select_one(".quick-fact-data-detail")) is not None
|
||||
hours: int = _int(_h1.text)
|
||||
hours: int = _int(_h1.text, failable=True)
|
||||
|
||||
return (days * 24) + hours
|
||||
|
||||
|
@ -520,7 +622,9 @@ def _get_top_overview(
|
|||
if len(top.select(select_needle)) == 0:
|
||||
continue
|
||||
|
||||
assert (_n := top.select(".listening-report-secondary-top-item-name")) is not None
|
||||
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
|
||||
|
@ -597,6 +701,34 @@ def _get_tracks_top_new(page: BeautifulSoup) -> ThingWithScrobbles:
|
|||
return _get_top_new_thing(page=page, select_needle=".top-new-item-type__track")
|
||||
|
||||
|
||||
def _get_tags(page: BeautifulSoup) -> dict[str, tuple[int, ...]]:
|
||||
tags: dict[str, tuple[int, ...]] = {}
|
||||
|
||||
assert (_1 := page.select_one("#top-tags-over-time")) is not None
|
||||
assert (_2 := _1.select_one(".js-top-tags-over-time-table")) is not None
|
||||
assert (_3 := _2.select_one("tbody")) is not None
|
||||
|
||||
for tr in _3.select("tr"):
|
||||
tag_name: str = ""
|
||||
tag_counts: list[int] = []
|
||||
|
||||
for i, td in enumerate(tr.select("td")):
|
||||
if i == 0:
|
||||
tag_name = td.text.strip()
|
||||
|
||||
else:
|
||||
assert (
|
||||
num := td.text.strip()
|
||||
).isnumeric(), (
|
||||
"second tag table td onwards should be numeric, but isn't"
|
||||
)
|
||||
tag_counts.append(int(num))
|
||||
|
||||
tags.update({tag_name: tuple(tag_counts)})
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def _rank(
|
||||
r: ListeningReport, rs: list[ListeningReport], k: Callable[[ListeningReport], int]
|
||||
):
|
||||
|
@ -605,12 +737,26 @@ def _rank(
|
|||
insort(ranking, _r, key=k)
|
||||
|
||||
for i, _r in enumerate(reversed(ranking), start=1):
|
||||
if _r.user == r.user:
|
||||
if _r == r:
|
||||
return i
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def calculate_tag_score(
|
||||
tag_values: tuple[int, ...], tags: dict[str, tuple[int, ...]]
|
||||
) -> float:
|
||||
v_cum_sum = sum([sum(tv) for tv in tags.values()])
|
||||
v_cum_avg = sum([(sum(tv) / len(tv)) for tv in tags.values()])
|
||||
|
||||
v_avg = (v_sum := sum(tag_values)) / len(tag_values)
|
||||
|
||||
tag_obj_score = (0.5 * v_avg) + (0.5 * v_sum)
|
||||
tag_cum_score = (0.5 * v_cum_sum) + (0.5 * v_cum_avg)
|
||||
|
||||
return 100 * (tag_obj_score / tag_cum_score)
|
||||
|
||||
|
||||
def make_circle_report(
|
||||
listening_reports: list[ListeningReport],
|
||||
behaviour: Behaviour,
|
||||
|
@ -627,6 +773,18 @@ def make_circle_report(
|
|||
case FormatTypeEnum.TELEGRAM:
|
||||
text.append(behaviour.header + "\n")
|
||||
|
||||
# pre-string building global tag versus calculation
|
||||
global_tag_counter: GlobalTagCounter = {}
|
||||
|
||||
for report in listening_reports:
|
||||
for tag_name, tag_value in report.tags.items():
|
||||
if tag_name not in global_tag_counter:
|
||||
global_tag_counter[tag_name] = Counter()
|
||||
|
||||
global_tag_counter[tag_name][report.user] += calculate_tag_score(
|
||||
tag_value, tags=report.tags
|
||||
) # type: ignore
|
||||
|
||||
for leaderboard_pos, report in enumerate(
|
||||
reversed(
|
||||
sorted(
|
||||
|
@ -643,24 +801,25 @@ def make_circle_report(
|
|||
leaderboard_scrobble_pos=_rank(
|
||||
r=report,
|
||||
rs=listening_reports,
|
||||
k=lambda r: r.listening_time_hours + r.scrobbles_count,
|
||||
k=lambda r: r.scrobbles_count,
|
||||
),
|
||||
leaderboard_artists_pos=_rank(
|
||||
r=report,
|
||||
rs=listening_reports,
|
||||
k=lambda r: r.listening_time_hours + r.artists_count,
|
||||
k=lambda r: r.artists_count,
|
||||
),
|
||||
leaderboard_albums_pos=_rank(
|
||||
r=report,
|
||||
rs=listening_reports,
|
||||
k=lambda r: r.listening_time_hours + r.albums_count,
|
||||
k=lambda r: r.albums_count,
|
||||
),
|
||||
leaderboard_tracks_pos=_rank(
|
||||
r=report,
|
||||
rs=listening_reports,
|
||||
k=lambda r: r.listening_time_hours + r.tracks_count,
|
||||
k=lambda r: r.tracks_count,
|
||||
),
|
||||
leaderboard_n=len(listening_reports),
|
||||
global_tag_counter=global_tag_counter,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
@ -674,7 +833,7 @@ def cli() -> None:
|
|||
reports: list[ListeningReport] = []
|
||||
|
||||
print(behaviour, file=stderr) if behaviour.verbose else ...
|
||||
for i, target in enumerate(behaviour.targets):
|
||||
for i, target in enumerate(set(behaviour.targets)):
|
||||
try:
|
||||
reports.append(
|
||||
get_listening_report(
|
||||
|
|
34
poetry.lock
generated
34
poetry.lock
generated
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
|
@ -330,13 +330,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.1"
|
||||
version = "4.2.2"
|
||||
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"},
|
||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -346,13 +346,13 @@ type = ["mypy (>=1.8)"]
|
|||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.31.0"
|
||||
version = "2.32.3"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -400,13 +400,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "types-beautifulsoup4"
|
||||
version = "4.12.0.20240504"
|
||||
version = "4.12.0.20240511"
|
||||
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"},
|
||||
{file = "types-beautifulsoup4-4.12.0.20240511.tar.gz", hash = "sha256:004f6096fdd83b19cdbf6cb10e4eae57b10205eccc365d0a69d77da836012e28"},
|
||||
{file = "types_beautifulsoup4-4.12.0.20240511-py3-none-any.whl", hash = "sha256:7ceda66a93ba28d759d5046d7fec9f4cad2f563a77b3a789efc90bcadafeefd1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -425,13 +425,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.31.0.20240406"
|
||||
version = "2.32.0.20240523"
|
||||
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"},
|
||||
{file = "types-requests-2.32.0.20240523.tar.gz", hash = "sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57"},
|
||||
{file = "types_requests-2.32.0.20240523-py3-none-any.whl", hash = "sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -439,13 +439,13 @@ urllib3 = ">=2"
|
|||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.11.0"
|
||||
version = "4.12.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"},
|
||||
{file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"},
|
||||
{file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "lfcircle"
|
||||
version = "0.1.1"
|
||||
version = "0.2.1"
|
||||
description = "last.fm statistics generator for your friend circle!"
|
||||
authors = ["Mark Joshwel <mark@joshwel.co>"]
|
||||
license = "Unlicense"
|
||||
|
|
7
qc.sh
Executable file
7
qc.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
black lfcircle.py --check
|
||||
isort lfcircle.py --check
|
||||
mypy lfcircle.py
|
||||
ruff check lfcircle.py
|
Loading…
Add table
Reference in a new issue