From 1788ad0f8c3789639264e6fdc596e772dcbdc879 Mon Sep 17 00:00:00 2001 From: Lucas Bickel <116588+hairmare@users.noreply.github.com> Date: Mon, 11 Mar 2024 02:10:52 +0100 Subject: [PATCH] chore(ci): configure ruff --- .pre-commit-config.yaml | 18 +- catalog-info.yaml | 2 +- conftest.py | 63 -- docs/gen_ref_pages.py | 10 +- nowplaying/__main__.py | 3 +- nowplaying/api.py | 79 +- nowplaying/daemon.py | 142 +-- nowplaying/input/handler.py | 36 +- nowplaying/input/observer.py | 226 +++-- nowplaying/main.py | 15 +- nowplaying/misc/saemubox.py | 19 +- nowplaying/options.py | 62 +- nowplaying/otel.py | 21 +- nowplaying/show/client.py | 98 +- nowplaying/show/show.py | 30 +- nowplaying/track/handler.py | 40 +- nowplaying/track/observers/base.py | 34 +- .../track/observers/dab_audio_companion.py | 15 +- nowplaying/track/observers/icecast.py | 78 +- nowplaying/track/observers/scrobbler.py | 3 +- nowplaying/track/observers/smc_ftp.py | 68 +- nowplaying/track/observers/ticker.py | 53 +- nowplaying/track/track.py | 66 +- nowplaying/util.py | 10 +- poetry.lock | 898 ++++++++++-------- pyproject.toml | 31 +- ruff.toml | 76 ++ tests/conftest.py | 58 +- tests/test_api.py | 64 +- tests/test_daemon.py | 6 +- tests/test_input_handler.py | 22 +- tests/test_input_observer.py | 5 +- tests/test_input_observer_base.py | 18 +- tests/test_input_observer_klangbecken.py | 10 +- tests/test_main.py | 4 +- tests/test_show_client.py | 57 +- tests/test_track.py | 10 +- .../test_track_observer_dabaudiocompanion.py | 22 +- tests/test_track_observer_icecast.py | 30 +- tests/test_track_observer_smc_ftp.py | 20 +- tests/test_track_observer_tickertrack.py | 18 +- 41 files changed, 1515 insertions(+), 1025 deletions(-) delete mode 100644 conftest.py create mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f9a8d7c..8b78c1bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,30 +1,18 @@ repos: - - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 - hooks: - - id: pyupgrade - args: - - --py311-plus - - repo: https://github.com/charliermarsh/ruff-pre-commit - # Ruff version. - rev: "v0.0.275" + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.3.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: local hooks: - - id: isort - name: isort - language: system - entry: isort - types: [python] - id: black name: black language: system entry: black types: [python] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace exclude: ^src/api/client.js$ diff --git a/catalog-info.yaml b/catalog-info.yaml index fae90d86..3ffeff90 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -3,7 +3,7 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: nowplaying - title: Nowplaging Service + title: Nowplaying Service description: | The nowplaying daemon is the python server central to our songticker. annotations: diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 9e4f6f16..00000000 --- a/conftest.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -import sys - -import pytest - -from nowplaying.show.show import Show -from nowplaying.track.observers.base import TrackObserver -from nowplaying.track.track import Track - -PACKAGE_PARENT = "nowplaying" -SCRIPT_DIR = os.path.dirname( - os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))) -) -sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))) - - -def new_show(name="Hairmare Traveling Medicine Show"): - s = Show() - s.set_name("Hairmare Traveling Medicine Show") - return s - - -@pytest.fixture() -def show_factory(): - """Return a method to help creating new show objects for tests.""" - return new_show - - -def new_track( - artist="Hairmare and the Band", - title="An Ode to legacy Python Code", - album="Live at the Refactoring Club", - duration=128, -): - t = Track() - t.set_artist(artist) - t.set_title(title) - t.set_album(album) - t.set_duration(duration) - return t - - -@pytest.fixture() -def track_factory(): - """Return a method to help creating new track objects for tests.""" - return new_track - - -class DummyObserver(TrackObserver): - """Shunt class for testing the abstract TrackObserver.""" - - pass - - def track_started(self, track): - pass - - def track_finished(self, track): - pass - - -@pytest.fixture() -def dummy_observer(): - return DummyObserver() diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index b1b8882a..fbc499ad 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -16,9 +16,7 @@ parts = list(module_path.parts) - if parts[-1] == "__init__": - continue - elif parts[-1] == "__main__": + if parts[-1] in ["__init__", "__main__"]: continue with mkdocs_gen_files.open(full_doc_path, "w") as fd: @@ -30,6 +28,8 @@ with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) -readme = Path("README.md").open("r") -with mkdocs_gen_files.open("index.md", "w") as index_file: +with Path("README.md").open("r") as readme, mkdocs_gen_files.open( + "index.md", + "w", +) as index_file: index_file.writelines(readme.read()) diff --git a/nowplaying/__main__.py b/nowplaying/__main__.py index e1062634..d77152ab 100644 --- a/nowplaying/__main__.py +++ b/nowplaying/__main__.py @@ -9,7 +9,8 @@ from .main import NowPlaying -def main(): +def main() -> None: + """Run nowplaying.""" NowPlaying().run() diff --git a/nowplaying/api.py b/nowplaying/api.py index 58c85d9f..35735cb0 100644 --- a/nowplaying/api.py +++ b/nowplaying/api.py @@ -1,8 +1,12 @@ +"""Nowplaying ApiServer.""" + +from __future__ import annotations + import json import logging -from queue import Queue +from typing import TYPE_CHECKING, Iterable, Self -import cherrypy +import cherrypy # type: ignore[import-untyped] import cridlib from cloudevents.exceptions import GenericException as CloudEventException from cloudevents.http import from_http @@ -10,6 +14,13 @@ from werkzeug.routing import Map, Rule from werkzeug.wrappers import Request, Response +if TYPE_CHECKING: # pragma: no cover + from queue import Queue + from wsgiref.types import StartResponse, WSGIEnvironment + + from nowplaying.options import Options + + logger = logging.getLogger(__name__) _RABE_CLOUD_EVENTS_SUBS = ( @@ -25,21 +36,27 @@ class ApiServer: """The API server.""" - def __init__(self, options, event_queue: Queue, realm: str = "nowplaying"): + def __init__( + self: Self, + options: Options, + event_queue: Queue, + realm: str = "nowplaying", + ) -> None: + """Create ApiServer.""" self.options = options self.event_queue = event_queue self.realm = realm self.url_map = Map([Rule("/webhook", endpoint="webhook")]) - def run_server(self): + def run_server(self: Self) -> None: """Run the API server.""" if self.options.debug: from werkzeug.serving import run_simple - self._server = run_simple( - self.options.apiBindAddress, - self.options.apiPort, + run_simple( + self.options.api_bind_address, + self.options.api_port, self, use_debugger=True, use_reloader=True, @@ -48,25 +65,35 @@ def run_server(self): cherrypy.tree.graft(self, "/") cherrypy.server.unsubscribe() - self._server = cherrypy._cpserver.Server() + self._server = cherrypy._cpserver.Server() # noqa: SLF001 - self._server.socket_host = self.options.apiBindAddress - self._server.socket_port = self.options.apiPort + self._server.socket_host = self.options.api_bind_address + self._server.socket_port = self.options.api_port self._server.subscribe() cherrypy.engine.start() cherrypy.engine.block() - def stop_server(self): + def stop_server(self: Self) -> None: """Stop the server.""" self._server.stop() cherrypy.engine.exit() - def __call__(self, environ, start_response): + def __call__( + self: Self, + environ: WSGIEnvironment, + start_response: StartResponse, + ) -> Iterable[bytes]: + """Forward calls to wsgi_app.""" return self.wsgi_app(environ, start_response) - def wsgi_app(self, environ, start_response): + def wsgi_app( + self: Self, + environ: WSGIEnvironment, + start_response: StartResponse, + ) -> Iterable[bytes]: + """Return a wsgi app.""" request = Request(environ) auth = request.authorization if auth and self.check_auth(auth.username, auth.password): @@ -75,13 +102,18 @@ def wsgi_app(self, environ, start_response): response = self.auth_required(request) return response(environ, start_response) - def check_auth(self, username, password): - return ( - username in self.options.apiAuthUsers - and self.options.apiAuthUsers[username] == password + def check_auth(self: Self, username: str | None, password: str | None) -> bool: + """Check if auth is valid.""" + return str( + username, + ) in self.options.api_auth_users and self.options.api_auth_users[ + str(username) + ] == str( + password, ) - def auth_required(self, request): + def auth_required(self: Self, _: Request) -> Response: + """Check if auth is required.""" return Response( "Could not verify your access level for that URL.\n" "You have to login with proper credentials", @@ -89,7 +121,8 @@ def auth_required(self, request): {"WWW-Authenticate": f'Basic realm="{self.realm}"'}, ) - def dispatch_request(self, request): + def dispatch_request(self: Self, request: Request) -> Response | HTTPException: + """Dispatch requests to handlers.""" adapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = adapter.match() @@ -101,25 +134,25 @@ def dispatch_request(self, request): {"Content-Type": "application/json"}, ) - def on_webhook(self, request): + def on_webhook(self: Self, request: Request) -> Response: """Receive a CloudEvent and put it into the event queue.""" logger.warning("Received a webhook") if ( request.headers.get("Content-Type") not in _RABE_CLOUD_EVENTS_SUPPORTED_MEDIA_TYPES ): - raise UnsupportedMediaType() + raise UnsupportedMediaType try: event = from_http(request.headers, request.data) except CloudEventException as error: - raise BadRequest(description=f"{error}") + raise BadRequest(description=str(error)) from error try: crid = cridlib.parse(event["id"]) logger.debug("Detected CRID: %s", crid) except cridlib.CRIDError as error: raise BadRequest( - description=f"CRID '{event['id']}' is not a RaBe CRID" + description=f"CRID '{event['id']}' is not a RaBe CRID", ) from error logger.info("Received event: %s", event) diff --git a/nowplaying/daemon.py b/nowplaying/daemon.py index 2f17333c..c2283d9d 100644 --- a/nowplaying/daemon.py +++ b/nowplaying/daemon.py @@ -1,3 +1,5 @@ +"""Nowplaying Daemon.""" + import logging import os import signal @@ -5,19 +7,24 @@ import time from queue import Queue from threading import Thread - -from cloudevents.http.event import CloudEvent +from typing import TYPE_CHECKING, Any, Self from .api import ApiServer -from .input import observer as inputObservers +from .input import observer as input_observers from .input.handler import InputHandler from .misc.saemubox import SaemuBox +from .options import Options from .track.handler import TrackEventHandler from .track.observers.dab_audio_companion import DabAudioCompanionTrackObserver from .track.observers.icecast import IcecastTrackObserver from .track.observers.smc_ftp import SmcFtpTrackObserver from .track.observers.ticker import TickerTrackObserver +if TYPE_CHECKING: # pragma: no cover + from cloudevents.http.event import CloudEvent + +_EXCEPTION_NOWPLAYING_MAIN = "Error in main" + logger = logging.getLogger(__name__) @@ -27,16 +34,20 @@ class NowPlayingDaemon: """initialize last_input to a know value.""" last_input = 1 - def __init__(self, options): + def __init__(self: Self, options: Options) -> None: + """Create NowPlayingDaemon.""" self.options = options - self.event_queue = Queue() + self.event_queue: Queue = Queue() self.saemubox = SaemuBox( - self.options.saemubox_ip, self.options.check_saemubox_sender + self.options.saemubox_ip, + self.options.check_saemubox_sender, ) - def main(self): # pragma: no cover - # TODO test once there is not saemubox in the loop + def main(self: Self) -> None: # pragma: no cover + """Run Daemon.""" + # TODO(hairmare): test once there is not saemubox in the loop + # https://github.com/radiorabe/nowplaying/issues/179 logger.info("Starting up now-playing daemon") self.saemubox.run() @@ -44,8 +55,8 @@ def main(self): # pragma: no cover self.register_signal_handlers() input_handler = self.get_input_handler() - except Exception as e: - logger.exception("Error: %s", e) + except Exception: + logger.exception(_EXCEPTION_NOWPLAYING_MAIN) sys.exit(-1) _thread = Thread(target=self._main_loop, args=(input_handler,)) @@ -54,19 +65,18 @@ def main(self): # pragma: no cover self._start_apiserver() # blocking - def _start_apiserver(self): + def _start_apiserver(self: Self) -> None: """Start the API server.""" self._api = ApiServer(self.options, self.event_queue) self._api.run_server() # blocking - def _stop_apiserver(self): + def _stop_apiserver(self: Self) -> None: """Stop the API server.""" logger.info("Stopping API server") self._api.stop_server() - def _main_loop(self, input_handler: InputHandler): # pragma: no cover - """ - Run main loop of the daemon. + def _main_loop(self: Self, input_handler: InputHandler) -> None: # pragma: no cover + """Run main loop of the daemon. Should be run in a thread. """ @@ -76,66 +86,68 @@ def _main_loop(self, input_handler: InputHandler): # pragma: no cover saemubox_id = self.poll_saemubox() while not self.event_queue.empty(): - logger.debug("Queue size: %i" % self.event_queue.qsize()) + logger.debug("Queue size: %i", self.event_queue.qsize()) event: CloudEvent = self.event_queue.get() logger.info( - "Handling update from event: %s, source: %s" - % (event["type"], event["source"]) + "Handling update from event: %s, source: %s", + event["type"], + event["source"], ) input_handler.update(saemubox_id, event) input_handler.update(saemubox_id) - except Exception as e: - logger.exception("Error: %s", e) + except Exception: + logger.exception(_EXCEPTION_NOWPLAYING_MAIN) - time.sleep(self.options.sleepSeconds) + time.sleep(self.options.sleep_seconds) - def register_signal_handlers(self): + def register_signal_handlers(self: Self) -> None: + """Register signal handler.""" logger.debug("Registering signal handler") signal.signal(signal.SIGINT, self.signal_handler) - # signal.signal(signal.SIGKIL, self.signal_handler) - def signal_handler(self, signum, frame): - logger.debug("Signal %i caught" % signum) + def signal_handler(self: Self, signum: int, *_: Any) -> None: # noqa: ANN401 + """Handle signals.""" + logger.debug("Signal %i caught", signum) - if signum == signal.SIGINT or signum == signal.SIGKILL: - logger.info("Signal %i caught, terminating." % signum) + if signum in [signal.SIGINT, signal.SIGKILL]: + logger.info("Signal %i caught, terminating.", signum) self._stop_apiserver() sys.exit(os.EX_OK) - def get_track_handler(self): # pragma: no cover - # TODO test once options have been refactored with v3 + def get_track_handler(self: Self) -> TrackEventHandler: # pragma: no cover + """Get TrackEventHandler.""" + # TODO(hairmare): test once options have been refactored with v3 + # https://github.com/radiorabe/nowplaying/issues/179 handler = TrackEventHandler() - [ + for url in self.options.icecast: handler.register_observer( IcecastTrackObserver( - # TODO v3 remove uername and password - # because we mandate specifying via url + # TODO(hairmare): v3 remove uername and password + # because we mandate specifying via url + # https://github.com/radiorabe/nowplaying/issues/179 options=IcecastTrackObserver.Options( url=url, username="source", - password=self.options.icecastPassword, - ) - ) + password=self.options.icecast_password, + ), + ), ) - for url in self.options.icecast - ] - [ + for url in self.options.dab: handler.register_observer( DabAudioCompanionTrackObserver( options=DabAudioCompanionTrackObserver.Options( - url=url, dl_plus=self.options.dab_send_dls - ) - ) + url=url, + dl_plus=self.options.dab_send_dls, + ), + ), ) - for url in self.options.dab - ] handler.register_observer( TickerTrackObserver( options=TickerTrackObserver.Options( - file_path=self.options.tickerOutputFile - ) - ) + file_path=self.options.ticker_output_file, + ), + ), ) if self.options.dab_smc: handler.register_observer( @@ -144,50 +156,50 @@ def get_track_handler(self): # pragma: no cover hostname=self.options.dab_smc_ftp_hostname, username=self.options.dab_smc_ftp_username, password=self.options.dab_smc_ftp_password, - ) - ) + ), + ), ) return handler - def get_input_handler(self): # pragma: no cover - # TODO test once options have been refactored with v3 + def get_input_handler(self: Self) -> InputHandler: # pragma: no cover + """Get InputHandler.""" + # TODO(hairmare): test once options have been refactored with v3 + # https://github.com/radiorabe/nowplaying/issues/179 handler = InputHandler() track_handler = self.get_track_handler() - klangbecken = inputObservers.KlangbeckenInputObserver( - self.options.currentShowUrl, self.options.inputFile + klangbecken = input_observers.KlangbeckenInputObserver( + self.options.current_show_url, + self.options.input_file, ) klangbecken.add_track_handler(track_handler) handler.register_observer(klangbecken) - nonklangbecken = inputObservers.NonKlangbeckenInputObserver( - self.options.currentShowUrl + nonklangbecken = input_observers.NonKlangbeckenInputObserver( + self.options.current_show_url, ) nonklangbecken.add_track_handler(track_handler) handler.register_observer(nonklangbecken) return handler - def poll_saemubox(self) -> int: # pragma: no cover - """ - Poll Saemubox for new data. + def poll_saemubox(self: Self) -> int: # pragma: no cover + """Poll Saemubox for new data. Should be run once per main loop. - TODO v3 remove once replaced with pathfinder + TODO(hairmare) v3 remove once replaced with pathfinder + https://github.com/radiorabe/nowplaying/issues/179 """ - saemubox_id = self.saemubox.get_active_output_id() - logger.debug("Sämubox id: %i" % saemubox_id) + logger.debug("Sämubox id: %i", saemubox_id) if self.last_input != saemubox_id: logger.info( - 'Sämubox changed from "%s" to "%s"' - % ( - self.saemubox.get_id_as_name(self.last_input), - self.saemubox.get_id_as_name(saemubox_id), - ) + 'Sämubox changed from "%s" to "%s"', + self.saemubox.get_id_as_name(self.last_input), + self.saemubox.get_id_as_name(saemubox_id), ) self.last_input = saemubox_id diff --git a/nowplaying/input/handler.py b/nowplaying/input/handler.py index abf51579..fdd3c4ff 100644 --- a/nowplaying/input/handler.py +++ b/nowplaying/input/handler.py @@ -1,12 +1,20 @@ +"""Observe all input.""" + +from __future__ import annotations + import logging import logging.handlers +from typing import TYPE_CHECKING, Self -from cloudevents.http.event import CloudEvent +if TYPE_CHECKING: # pragma: no cover + from cloudevents.http.event import CloudEvent -from .observer import InputObserver + from nowplaying.input.observer import InputObserver logger = logging.getLogger(__name__) +_EXCEPTION_INPUT_UPDATE_FAIL = "Failed to update observer." + class InputHandler: """Inform all registered input-event observers about an input status. @@ -14,23 +22,27 @@ class InputHandler: This is the subject of the classical observer pattern. """ - def __init__(self): + def __init__(self: Self) -> None: + """Create InputHandler.""" self._observers: list[InputObserver] = [] - def register_observer(self, observer: InputObserver): - logger.info("Registering InputObserver '%s'" % observer.__class__.__name__) + def register_observer(self: Self, observer: InputObserver) -> None: + """Register an observer.""" + logger.info("Registering InputObserver '%s'", observer.__class__.__name__) self._observers.append(observer) - def remove_observer(self, observer: InputObserver): + def remove_observer(self: Self, observer: InputObserver) -> None: + """Remove an observer.""" self._observers.remove(observer) - def update(self, saemubox_id: int, event: CloudEvent = None): + def update(self: Self, saemubox_id: int, event: CloudEvent | None = None) -> None: + """Update all observers.""" for observer in self._observers: - logger.debug("Sending update event to observer %s" % observer.__class__) + logger.debug("Sending update event to observer %s", observer.__class__) try: observer.update(saemubox_id, event) - except Exception as e: # pragma: no cover - # TODO test once replaced with non generic exception - logger.error(f"InputObserver ({observer.__class__}): {e}") - logger.exception(e) + except Exception: # pragma: no cover + # TODO(hairmare): test once replaced with non generic exception + # https://github.com/radiorabe/nowplaying/issues/180 + logger.exception(_EXCEPTION_INPUT_UPDATE_FAIL) diff --git a/nowplaying/input/observer.py b/nowplaying/input/observer.py index 881eafe8..7d96e2f7 100644 --- a/nowplaying/input/observer.py +++ b/nowplaying/input/observer.py @@ -1,22 +1,33 @@ +"""Nowplaying input observer.""" + +from __future__ import annotations + import logging import logging.handlers -import os import time import warnings import xml.dom.minidom from abc import ABC, abstractmethod +from pathlib import Path +from typing import TYPE_CHECKING, Self -import isodate +import isodate # type: ignore[import-untyped] import pytz -from cloudevents.http.event import CloudEvent -from ..show import client -from ..show.show import Show -from ..track.handler import TrackEventHandler -from ..track.track import DEFAULT_ARTIST, DEFAULT_TITLE, Track +from nowplaying.show import client +from nowplaying.show.show import Show +from nowplaying.track.track import DEFAULT_ARTIST, DEFAULT_TITLE, Track + +if TYPE_CHECKING: # pragma: no cover + from cloudevents.http.event import CloudEvent + + from nowplaying.track.handler import TrackEventHandler logger = logging.getLogger(__name__) +_EXCEPTION_INPUT_MISSING_SONG_TAG = "No tag found" +_EXCEPTION_INPUT_MISSING_TIMESTAMP = "Song timestamp attribute is missing" + class InputObserver(ABC): """Abstract base for all InputObservers.""" @@ -24,7 +35,8 @@ class InputObserver(ABC): _SHOW_NAME_KLANGBECKEN = "Klangbecken" _SHOW_URL_KLANGBECKEN = "http://www.rabe.ch/sendungen/musik/klangbecken.html" - def __init__(self, current_show_url: str): + def __init__(self: Self, current_show_url: str) -> None: + """Create InputObserver.""" self.show: Show self.track_handler: TrackEventHandler self.previous_saemubox_id: int = -1 @@ -36,95 +48,129 @@ def __init__(self, current_show_url: str): self.showclient = client.ShowClient(current_show_url) self.show = self.showclient.get_show_info() - def add_track_handler(self, track_handler: TrackEventHandler): + def add_track_handler(self: Self, track_handler: TrackEventHandler) -> None: + """Add Track handler.""" self.track_handler = track_handler - def update(self, saemubox_id: int, event: CloudEvent = None): - # TODO v3-prep refactor to use :meth:`handles` instead of :meth:`handle_id` + def update(self: Self, saemubox_id: int, event: CloudEvent | None = None) -> None: + """Handle update.""" + # TODO(hairmare): v3-prep refactor to use :meth:`handles` + # instead of :meth:`handle_id` + # https://github.com/radiorabe/nowplaying/issues/180 if self.handle_id(saemubox_id, event): self.handle(event) @abstractmethod - # TODO v3 remove this method + # TODO(hairmare): v3 remove this method + # https://github.com/radiorabe/nowplaying/issues/179 def handle_id( - self, saemubox_id: int, event: CloudEvent = None - ): # pragma: no coverage - pass + self: Self, + saemubox_id: int, + event: CloudEvent | None = None, + ) -> bool: # pragma: no coverage + """Handle ID.""" @abstractmethod - # TODO v3 remove this method - def handle(self, event: CloudEvent = None): # pragma: no coverage - pass + # TODO(hairmare): v3 remove this method + # https://github.com/radiorabe/nowplaying/issues/179 + def handle( + self: Self, + event: CloudEvent | None = None, + ) -> None: # pragma: no coverage + """Handle event.""" @abstractmethod - def handles(self, event: CloudEvent) -> bool: # pragma: no coverage - pass + def handles(self: Self, event: CloudEvent) -> bool: # pragma: no coverage + """Handle event.""" @abstractmethod - def event(self, event: CloudEvent): # pragma: no coverage - pass + def event(self: Self, event: CloudEvent) -> None: # pragma: no coverage + """Handle event.""" class KlangbeckenInputObserver(InputObserver): """Observe when Sämu Box says Klangbecken we have now-playing.xml input.""" def __init__( - self, current_show_url: str, input_file: str = None - ): # pragma: no coverage - # TODO test once input file is replaced with api + self: Self, + current_show_url: str, + input_file: str | None = None, + ) -> None: # pragma: no coverage + """Create KlangbeckenInputObserver.""" + # TODO(hairmare): test once input file is replaced with api + # https://github.com/radiorabe/nowplaying/issues/180 if input_file: warnings.warn( "The now-playing.xml format from Loopy/Klangbecken " "will be replaced in the future", PendingDeprecationWarning, + stacklevel=2, ) self.input_file = input_file - self.last_modify_time = os.stat(self.input_file).st_mtime + self.last_modify_time = Path(self.input_file).stat().st_mtime self.track: Track super().__init__(current_show_url) - def handles(self, event: CloudEvent) -> bool: - # TODO v3-prep call :meth:`handle_id` from here - # needs saemubox_id compat workaround - # TODO v3 remove call to :meth:`handle_id` - # TODO make magic string configurable - # TODO check if source is currently on-air + def handles(self: Self, event: CloudEvent | None) -> bool: + """Check if we need to handle the event.""" + # TODO(hairmare): v3-prep call :meth:`handle_id` from here + # needs saemubox_id compat workaround + # https://github.com/radiorabe/nowplaying/issues/180 + # TODO(hairmare): v3 remove call to :meth:`handle_id` + # https://github.com/radiorabe/nowplaying/issues/179 + # TODO(hairmare): make magic string configurable + # https://github.com/radiorabe/nowplaying/issues/179 + # TODO(hairmare): check if source is currently on-air + # https://github.com/radiorabe/nowplaying/issues/179 if not event: # pragma: no coverage - # TODO remove checking for None once only events exist + # TODO(hairmare): remove checking for None once only events exist + # https://github.com/radiorabe/nowplaying/issues/179 return False return event["source"] == "https://github/radiorabe/klangbecken" - def event(self, event: CloudEvent): + def event(self: Self, event: CloudEvent) -> None: + """Handle event.""" self._handle(event) - def handle_id(self, saemubox_id: int, event: CloudEvent = None): + def handle_id( + self: Self, + saemubox_id: int, + event: CloudEvent | None = None, + ) -> bool: + """Handle ID.""" # only handle Klangbecken output if saemubox_id == 1: return True - # TODO v3-prep make this get called from :meth:`handles` + # TODO(hairmare): v3-prep make this get called from :meth:`handles` + # https://github.com/radiorabe/nowplaying/issues/180 return self.handles(event) - def handle(self, event: CloudEvent = None): + def handle(self: Self, event: CloudEvent | None = None) -> None: + """Handle RaBe CloudEevent.""" self._handle(event) - def _handle(self, event: CloudEvent = None): + def _handle(self: Self, event: CloudEvent | None = None) -> None: """Handle actual RaBe CloudEevent. - TODO v3: move into :meth:`event` - once :meth:`handle` and :meth:`handle_id` have been yeeted - TODO v3: remove all refs to input_file and it's modify time - once we use event handlers + TODO(hairmare): v3: move into :meth:`event` + once :meth:`handle` and :meth:`handle_id` have been yeeted + https://github.com/radiorabe/nowplaying/issues/179 + TODO(hairmare): v3: remove all refs to input_file and it's modify time + once we use event handlers + https://github.com/radiorabe/nowplaying/issues/179 """ if not event: - # @TODO: replace the stat method with inotify - modify_time = os.stat(self.input_file).st_mtime + # @TODO(hairmare): replace the stat method with inotify + # https://github.com/radiorabe/nowplaying/issues/179 + modify_time = Path(self.input_file).stat().st_mtime - # @TODO: Need to check if we have a stale file and send default - # track infos in this case. This might happend if loopy - # went out for lunch... - # pseudo code: now > modify_time + self.track.get_duration() + # @TODO(hairmare): Need to check if we have a stale file and send default + # track infos in this case. This might happend if loopy + # went out for lunch... + # pseudo code: now > modify_time + self.track.get_duration() + # https://github.com/radiorabe/nowplaying/issues/179 if self.first_run or event or modify_time > self.last_modify_time: logger.info("Now playing file changed") @@ -137,16 +183,18 @@ def _handle(self, event: CloudEvent = None): self.track = self.parse_event(event) self.first_run = False - logger.info("First run: %s" % self.first_run) + logger.info("First run: %s", self.first_run) if not self.first_run: # pragma: no coverage - # TODO test once we don't have to care about - # mtime/inotify because it's an api + # TODO(hairmare): test once we don't have to care about + # mtime/inotify because it's an api + # https://github.com/radiorabe/nowplaying/issues/179 logger.info("calling track_finished") self.track_handler.track_finished(self.track) if not event: - # TODO remove once legacy xml is gone + # TODO(hairmare): remove once legacy xml is gone + # https://github.com/radiorabe/nowplaying/issues/179 self.track = self.get_track_info() # Klangbecken acts as a failover and last resort input, if other @@ -155,8 +203,9 @@ def _handle(self, event: CloudEvent = None): # of what loopy thinks. if self.show.name != self._SHOW_NAME_KLANGBECKEN: logger.info( - "Klangbecken Input active, overriding current show '%s' with '%s'" - % (self.show.name, self._SHOW_NAME_KLANGBECKEN) + "Klangbecken Input active, overriding current show '%s' with '%s'", + self.show.name, + self._SHOW_NAME_KLANGBECKEN, ) self.show = Show() @@ -170,14 +219,17 @@ def _handle(self, event: CloudEvent = None): self.track.set_show(self.show) - # TODO: or finished? + # TODO(hairmare): or finished? + # https://github.com/radiorabe/nowplaying/issues/179 self.track_handler.track_started(self.track) self.first_run = False - def get_track_info(self): - # TODO v3 remove method once legacy xml is gone - dom = xml.dom.minidom.parse(self.input_file) + def get_track_info(self: Self) -> Track: + """Get Track info.""" + # TODO(hairmare): v3 remove method once legacy xml is gone + # https://github.com/radiorabe/nowplaying/issues/179 + dom = xml.dom.minidom.parse(self.input_file) # noqa: S318 # default track info track_info = { @@ -188,38 +240,41 @@ def get_track_info(self): "time": "", } - song = dom.getElementsByTagName("song") + songs = dom.getElementsByTagName("song") - if len(song) == 0 or song[0].hasChildNodes() is False: # pragma: no coverage - # TODO replace with non generic exception and test - raise Exception("No tag found") + if len(songs) == 0 or songs[0].hasChildNodes() is False: # pragma: no coverage + # TODO(hairmare): replace with non generic exception and test + # https://github.com/radiorabe/nowplaying/issues/179 + raise Exception(_EXCEPTION_INPUT_MISSING_SONG_TAG) # noqa: TRY002 - song = song[0] + song = songs[0] for name in list(track_info.keys()): elements = song.getElementsByTagName(name) if len(elements) == 0: # pragma: no coverage - # TODO replace with non generic exception and test - raise Exception("No <%s> tag found" % name) - elif elements[0].hasChildNodes(): - element_data = elements[0].firstChild.data.strip() + # TODO(hairmare): replace with non generic exception and test + # https://github.com/radiorabe/nowplaying/issues/179 + raise Exception("No <%s> tag found" % name) # noqa: TRY002 + if elements[0].hasChildNodes(): + element_data = elements[0].firstChild.data.strip() # type: ignore[attr-defined,union-attr] if element_data != "": track_info[name] = element_data else: # pragma: no coverage - logger.info("Element %s has empty value, ignoring" % name) + logger.info("Element %s has empty value, ignoring", name) if not song.hasAttribute("timestamp"): # pragma: no coverage - # TODO replace with non generic exception and test - raise Exception("Song timestamp attribute is missing") + # TODO(hairmare): replace with non generic exception and test + # https://github.com/radiorabe/nowplaying/issues/179 + raise Exception(_EXCEPTION_INPUT_MISSING_TIMESTAMP) # noqa: TRY002 # set the start time and append the missing UTC offset # @TODO: The UTC offset should be provided by the now playing XML # generated by Thomas # ex.: 2012-05-15T09:47:07+02:00 track_info["start_timestamp"] = song.getAttribute("timestamp") + time.strftime( - "%z" + "%z", ) current_track = Track() @@ -231,17 +286,18 @@ def get_track_info(self): # Store as UTC datetime object current_track.set_starttime( isodate.parse_datetime(track_info["start_timestamp"]).astimezone( - pytz.timezone("UTC") - ) + pytz.timezone("UTC"), + ), ) - current_track.set_duration(track_info["time"]) + current_track.set_duration(int(track_info["time"])) return current_track - def parse_event(self, event: CloudEvent) -> Track: + def parse_event(self: Self, event: CloudEvent) -> Track: + """Parse event.""" track = Track() - logger.info("Parsing event: %s" % event) + logger.info("Parsing event: %s", event) track.set_artist(event.data["item.artist"]) track.set_title(event.data["item.title"]) @@ -250,13 +306,12 @@ def parse_event(self, event: CloudEvent) -> Track: if event["type"] == "ch.rabe.api.events.track.v1.trackStarted": track.set_starttime(event_time) elif event["type"] == "ch.rabe.api.events.track.v1.trackFinished": - # TODO consider using now() instead of event['time'] track.set_endtime(event_time) if "item.length" in event.data: track.set_duration(event.data["item.length"]) - logger.info("Track: %s" % track) + logger.info("Track: %s", track) return track @@ -266,7 +321,7 @@ class NonKlangbeckenInputObserver(InputObserver): Uses the show's name instead of the actual track infos """ - def handles(self, event: CloudEvent) -> bool: # pragma: no coverage + def handles(self: Self, _: CloudEvent) -> bool: # pragma: no coverage """Do not handle events yet. TODO implement this method @@ -276,19 +331,20 @@ def handles(self, event: CloudEvent) -> bool: # pragma: no coverage """ return False - def event(self, event: CloudEvent): # pragma: no coverage + def event(self: Self, event: CloudEvent) -> None: # pragma: no coverage """Do not handle events yet. TODO implement this method """ super().event(event) - def handle_id(self, saemubox_id: int, event: CloudEvent = None): + def handle_id(self: Self, saemubox_id: int, _: CloudEvent | None = None) -> bool: + """Handle new ID from Saemubox.""" if saemubox_id != self.previous_saemubox_id: # If sämubox changes, force a show update, this acts as # a self-healing measurement in case the show web service provides # nonsense ;) - self.show = self.showclient.get_show_info(True) + self.show = self.showclient.get_show_info(force_update=True) self.previous_saemubox_id = saemubox_id @@ -298,7 +354,8 @@ def handle_id(self, saemubox_id: int, event: CloudEvent = None): return False - def handle(self, event: CloudEvent = None): + def handle(self: Self, _: CloudEvent | None = None) -> None: + """Handle Track.""" self.show = self.showclient.get_show_info() # only handle if a new show has started @@ -307,7 +364,8 @@ def handle(self, event: CloudEvent = None): self.track_handler.track_started(self.get_track_info()) self.previous_show_uuid = self.show.uuid - def get_track_info(self): + def get_track_info(self: Self) -> Track: + """Get Track info.""" current_track = Track() current_track.set_artist(DEFAULT_ARTIST) diff --git a/nowplaying/main.py b/nowplaying/main.py index 821758c9..b211b8d3 100644 --- a/nowplaying/main.py +++ b/nowplaying/main.py @@ -1,4 +1,7 @@ +"""Nowplaying entrypoint.""" + import socket +from typing import Self from .daemon import NowPlayingDaemon from .options import Options @@ -6,20 +9,22 @@ class NowPlaying: - def run(self): + """Nowplaying main class.""" + + def run(self: Self) -> None: """Load configuration, initialize environment and start nowplaying daemon.""" self.options = Options() self.options.parse_known_args() self._setup_otel() - socket.setdefaulttimeout(self.options.socketDefaultTimeout) + socket.setdefaulttimeout(self.options.socket_default_timeout) self._run_daemon() - def _setup_otel(self): # pragma: no cover + def _setup_otel(self: Self) -> None: # pragma: no cover if not self.options.debug: - setup_otel(self.options.otlp_enable) + setup_otel(otlp_enable=self.options.otlp_enable) - def _run_daemon(self): + def _run_daemon(self: Self) -> None: """Start nowplaying daemon.""" NowPlayingDaemon(self.options).main() diff --git a/nowplaying/misc/saemubox.py b/nowplaying/misc/saemubox.py index 61a481e6..fc500ebd 100644 --- a/nowplaying/misc/saemubox.py +++ b/nowplaying/misc/saemubox.py @@ -20,8 +20,6 @@ class SaemuBoxError(Exception): """SaemuBox related exception.""" - pass - class SaemuBox: """Receive and validate info from Sämu Box for nowplaying.""" @@ -37,7 +35,8 @@ class SaemuBox: def __init__(self, saemubox_ip, check_sender=True): warnings.warn( - "Saemubox will be replaced with Pathfinder", PendingDeprecationWarning + "Saemubox will be replaced with Pathfinder", + PendingDeprecationWarning, ) self.output = "" @@ -70,11 +69,11 @@ def _setup_socket(self): # pragma: no cover except OSError as e: # pragma: no cover self.sock = None logger.error("SaemuBox: cannot bind to %s:%i." % (self.bind_ip, self.port)) - raise SaemuBoxError() from e + raise SaemuBoxError from e def __update(self): # pragma: no cover if self.sock is None or (hasattr(self.sock, "_closed") and self.sock._closed): - logger.warn("SaemuBox: socket closed unexpectedly, retrying...") + logger.warning("SaemuBox: socket closed unexpectedly, retrying...") self._setup_socket() output = None @@ -84,7 +83,9 @@ def __update(self): # pragma: no cover while select.select([self.sock], [], [], 0)[0]: data, addr = self.sock.recvfrom(1024) if self.check_sender and addr[0] not in self.senders: - logger.warn("SaemuBox: receiving data from invalid host: %s " % addr[0]) + logger.warning( + "SaemuBox: receiving data from invalid host: %s " % addr[0], + ) continue ids = data.split() # several saemubox ids might come in one packet @@ -94,7 +95,7 @@ def __update(self): # pragma: no cover seen_senders.add(addr[0]) output = id else: - logger.warn("SaemuBox: received invalid data: %s" % data) + logger.warning("SaemuBox: received invalid data: %s" % data) if output is None: logger.error("SaemuBox: could not read current status.") @@ -102,7 +103,7 @@ def __update(self): # pragma: no cover raise SaemuBoxError("Cannot read data from SaemuBox") elif seen_senders != self.senders: for missing_sender in self.senders - seen_senders: - logger.warn("SaemuBox: missing sender: %s" % missing_sender) + logger.warning("SaemuBox: missing sender: %s" % missing_sender) self.output = int(output) @@ -126,7 +127,7 @@ def get_id_as_name(self, number): # pragma: no cover logger.addHandler(logging.StreamHandler(sys.stdout)) logger.setLevel(logging.INFO) - sb = SaemuBox() + sb = SaemuBox(saemubox_ip="127.0.0.1") localhost = socket.gethostbyname(socket.gethostname()) sb.senders.add(localhost) diff --git a/nowplaying/options.py b/nowplaying/options.py index da212c94..c206d257 100644 --- a/nowplaying/options.py +++ b/nowplaying/options.py @@ -1,4 +1,10 @@ -import configargparse +"""Options for Nowplaying.""" + +from __future__ import annotations + +from typing import Self + +import configargparse # type: ignore[import-untyped] from nowplaying.track.observers.dab_audio_companion import ( DabAudioCompanionTrackObserver, @@ -12,17 +18,19 @@ class Options: """Contain all hardcoded and loaded from configargparse options.""" """How many seconds the main daemon loop sleeps.""" - sleepSeconds = 1 + sleep_seconds = 1 """Default socket of 2 minutes, to prevent endless hangs on HTTP requests.""" - socketDefaultTimeout = 120 + socket_default_timeout = 120 - def __init__(self): + def __init__(self: Self) -> None: """Configure configargparse.""" self.__args = configargparse.ArgParser( - default_config_files=["/etc/nowplaying/conf.d/*.conf", "~/.nowplayingrc"] + default_config_files=["/etc/nowplaying/conf.d/*.conf", "~/.nowplayingrc"], ) - # TODO v3 remove this option + # TODO(hairmare): v3 remove this option + # https://github.com/radiorabe/nowplaying/issues/179 + self.saemubox_ip: str | None = None self.__args.add_argument( "-b", "--saemubox-ip", @@ -30,52 +38,75 @@ def __init__(self): help="IP address of SAEMUBOX", default="", ) - # TODO v3 remove this option + # TODO(hairmare): v3 remove this option + # https://github.com/radiorabe/nowplaying/issues/179 + self.check_saemubox_sender: bool = True self.__args.add_argument( "--check-saemubox-sender", dest="check_saemubox_sender", help="Check SRC SAEMUBOX IP", default=True, ) + + self.icecast: list[str] = [] + self.icecast_password: str = "" IcecastTrackObserver.Options.args(self.__args) + + self.dab: list[str] = [] + self.dab_send_dls: bool = False DabAudioCompanionTrackObserver.Options.args(self.__args) + + self.dab_smc: bool = False + self.dab_smc_ftp_hostname: str = "" + self.dab_smc_ftp_username: str = "" + self.dab_smc_ftp_password: str = "" SmcFtpTrackObserver.Options.args(self.__args) + + self.ticker_output_file: str = "" TickerTrackObserver.Options.args(self.__args) + + self.current_show_url: str = "" self.__args.add_argument( "-s", "--show", - dest="currentShowUrl", + dest="current_show_url", help="Current Show URL e.g. 'https://libretime.int.example.org/api/live-info-v2/format/json'", ) - # TODO v3 remove this option + # TODO(hairmare): v3 remove this option + # https://github.com/radiorabe/nowplaying/issues/179 + self.input_file: str = "/home/endlosplayer/Eingang/now-playing.xml" self.__args.add_argument( "--input-file", - dest="inputFile", + dest="input_file", help=( "XML 'now-playing' input file location, " "disable input by passing empty string, ie. --input-file=''" ), default="/home/endlosplayer/Eingang/now-playing.xml", ) + self.api_bind_address: str = "127.0.0.1" self.__args.add_argument( "--api-bind-address", - dest="apiBindAddress", + dest="api_bind_address", help="Bind address for the API server", - default="0.0.0.0", + default="127.0.0.1", ) + self.api_port: int = 8080 self.__args.add_argument( "--api-port", type=int, - dest="apiPort", + dest="api_port", help="Bind port for the API server", default=8080, ) + self.api_auth_users: dict[str, str] = {} self.__args.add_argument( "--api-auth-users", - dest="apiAuthUsers", + dest="api_auth_users", help="API Auth Users", default={"rabe": "rabe"}, ) + self.otlp_enable: bool = False self.__args.add_argument( "--instrumentation-otlp-enable", type=bool, @@ -86,6 +117,7 @@ def __init__(self): default=False, env_var="NOWPLAYING_INSTRUMENTATION_OTLP_ENABLE", ) + self.debug: bool = False self.__args.add_argument( "--debug", type=bool, @@ -96,6 +128,6 @@ def __init__(self): default=False, ) - def parse_known_args(self): + def parse_known_args(self: Self) -> None: """Parse known args with configargparse.""" self.__args.parse_known_args(namespace=self) diff --git a/nowplaying/otel.py b/nowplaying/otel.py index 0c5429bd..67c8f274 100644 --- a/nowplaying/otel.py +++ b/nowplaying/otel.py @@ -6,10 +6,13 @@ import logging import os from datetime import datetime +from pathlib import Path +from typing import Self, no_type_check from opentelemetry._logs import set_logger_provider from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs._internal import LogRecord from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ConsoleLogExporter, @@ -18,9 +21,10 @@ from opentelemetry.sdk.resources import Resource -def _log_formatter(record): # pragma: no cover +@no_type_check +def _log_formatter(record: LogRecord) -> str: # pragma: no cover return ( - f"{datetime.fromtimestamp(record.timestamp/1000000000)} " + f"{datetime.fromtimestamp(record.timestamp/1000000000)} " # noqa: DTZ006 f"- {record.severity_text[:4]:4} " f"- {record.attributes['source_name'][11:]:14} " f"- {record.body} " @@ -32,16 +36,19 @@ def _log_formatter(record): # pragma: no cover class SourceAttributeFilter(logging.Filter): # pragma: no cover """Used on the handler to ensure that some attributes are carried over to otel.""" - def filter(self, record) -> bool: + @no_type_check + def filter(self: Self, record: LogRecord) -> bool: + """Carry over attributes to otel.""" record.source_name = record.name record.source_pathname = os.path.relpath( - record.pathname, os.path.dirname(os.path.dirname(__file__)) + record.pathname, + Path(__file__).parent.parent, ) record.source_lineno = record.lineno return True -def setup_otel(otlp_enable=False): # pragma: no cover +def setup_otel(*, otlp_enable: bool = False) -> None: # pragma: no cover """Configure opentelemetry logging to stdout and collector.""" root = logging.getLogger() root.setLevel(logging.INFO) @@ -51,12 +58,12 @@ def setup_otel(otlp_enable=False): # pragma: no cover { "service.name": "nowplaying", }, - ) + ), ) set_logger_provider(logger_provider) console_exporter = ConsoleLogExporter( - formatter=lambda record: _log_formatter(record) + formatter=lambda record: _log_formatter(record), ) logger_provider.add_log_record_processor(SimpleLogRecordProcessor(console_exporter)) diff --git a/nowplaying/show/client.py b/nowplaying/show/client.py index 5f22f57d..273ed565 100644 --- a/nowplaying/show/client.py +++ b/nowplaying/show/client.py @@ -1,8 +1,13 @@ +"""Show client interacts with LibreTime.""" + +from __future__ import annotations + import datetime import logging import logging.handlers import re from html.entities import entitydefs +from typing import Match, Self import pytz import requests @@ -11,12 +16,15 @@ logger = logging.getLogger(__name__) +_EXCEPTION_SHOWCLIENT_NO_SHOW = "Unable to get current show information" +_EXCEPTION_SHOWCLIENT_NO_NAME = "Missing show name" +_EXCEPTION_SHOWCLIENT_NO_START = "Missing show start time" +_EXCEPTION_SHOWCLIENT_NO_END = "Missing show end time" + class ShowClientError(Exception): """ShowClient related exception.""" - pass - class ShowClient: """Fetches the show info from LibreTime now-playing v2 endpoint. @@ -28,15 +36,15 @@ class ShowClient: __cleanup_show_name_regexp = re.compile(r"&(\w+?);") __show_datetime_format = "%Y-%m-%d %H:%M:%S" - def __init__(self, current_show_url): + def __init__(self: Self, current_show_url: str) -> None: + """Create Show.""" self.current_show_url = current_show_url self.show = Show() - self.showtz = None + self.showtz = pytz.timezone(zone="UTC") - def get_show_info(self, force_update=False): + def get_show_info(self: Self, *, force_update: bool = False) -> Show: """Return a Show object.""" - if force_update: self.update() else: @@ -44,8 +52,8 @@ def get_show_info(self, force_update=False): return self.show - def lazy_update(self): - # only update the info if we expect that a new show has started + def lazy_update(self: Self) -> None: + """Only update the info if we expect that a new show has started.""" if datetime.datetime.now(pytz.timezone("UTC")) > self.show.endtime: logger.info("Show expired, going to update show info") self.update() @@ -53,7 +61,8 @@ def lazy_update(self): else: logger.debug("Show still running, won't update show info") - def update(self): + def update(self: Self) -> None: + """Update state.""" self.show = Show() # Create a new show object # Set the show's default end time to now + 30 seconds to prevent updates @@ -61,25 +70,22 @@ def update(self): # goes wrong later. self.show.set_endtime( datetime.datetime.now(pytz.timezone("UTC")) - + datetime.timedelta(seconds=self.__DEFAULT_SHOW_DURATION) + + datetime.timedelta(seconds=self.__DEFAULT_SHOW_DURATION), ) try: # try to get the current show informations from loopy's cast web # service - data = requests.get(self.current_show_url).json() - - logger.debug("Got show info: %s" % data) + data = requests.get(self.current_show_url, timeout=60).json() - except Exception as e: - logger.error("Unable to get current show informations") + logger.debug("Got show info: %s", data) - logger.exception(e) - # LSB 2017: ignoring missing show update - # raise ShowClientError('Unable to get show informations: %s' % e) + except Exception: + logger.exception(_EXCEPTION_SHOWCLIENT_NO_SHOW) + # ignoring missing show update return - self.showtz = pytz.timezone(data["station"]["timezone"]) + self.showtz = pytz.timezone(zone=data["station"]["timezone"]) # pick the current show show_data = self.__pick_current_show(data) @@ -94,8 +100,7 @@ def update(self): if len(real_name) == 0: # keep the default show information - logger.error("No show name found") - raise ShowClientError("Missing show name") + raise ShowClientError(_EXCEPTION_SHOWCLIENT_NO_NAME) real_name = self.__cleanup_show_name(real_name) self.show.set_name(real_name) @@ -105,11 +110,13 @@ def update(self): end_time = show_data["ends"] if len(end_time) == 0: - logger.error("No end found") - raise ShowClientError("Missing show end time") + raise ShowClientError(_EXCEPTION_SHOWCLIENT_NO_END) endtime = self.showtz.localize( - datetime.datetime.strptime(end_time, self.__show_datetime_format) + datetime.datetime.strptime( # noqa: DTZ007 + end_time, + self.__show_datetime_format, + ), ) # store as UTC datetime object @@ -121,10 +128,13 @@ def update(self): if len(start_time) == 0: logger.error("No start found") - raise ShowClientError("Missing show start time") + raise ShowClientError(_EXCEPTION_SHOWCLIENT_NO_START) starttime = self.showtz.localize( - datetime.datetime.strptime(start_time, self.__show_datetime_format) + datetime.datetime.strptime( # noqa: DTZ007 + start_time, + self.__show_datetime_format, + ), ) # store as UTC datetime object @@ -134,10 +144,10 @@ def update(self): # This prevents stale (wrong) show informations from beeing pushed to # the live stream and stops hammering the service every second if self.show.endtime < datetime.datetime.now(pytz.timezone("UTC")): - logger.error("Show endtime %s is in the past" % self.show.endtime) + logger.error("Show endtime %s is in the past", self.show.endtime) raise ShowClientError( - "Show end time (%s) is in the past" % self.show.endtime + "Show end time (%s) is in the past" % self.show.endtime, ) # get the show's URL @@ -150,15 +160,17 @@ def update(self): self.show.set_url(url) logger.info( - 'Show "%s" started and runs from %s till %s' - % (self.show.name, starttime, endtime) + 'Show "%s" started and runs from %s till %s', + self.show.name, + starttime, + endtime, ) logger.debug(self.show) - def __cleanup_show_name(self, name) -> str: + def __cleanup_show_name(self: Self, name: str) -> str: """Cleanup name by undoing htmlspecialchars from libretime zf1 mvc.""" - def __entityref_decode(m): + def __entityref_decode(m: Match[str]) -> str: try: return entitydefs[m.group(1)] except KeyError: @@ -166,7 +178,7 @@ def __entityref_decode(m): return self.__cleanup_show_name_regexp.sub(__entityref_decode, name) - def __pick_current_show(self, data): + def __pick_current_show(self: Self, data: dict[str, dict]) -> dict[str, str] | None: """Pick the current show from the data. If there is no current show and the next one starts reasonably soon, pick that. @@ -176,21 +188,25 @@ def __pick_current_show(self, data): logger.info("No current show is playing, checking next show") if data["shows"]["next"] and data["shows"]["next"][0]: show = data["shows"]["next"][0] - logger.info("Next show is %s" % show["name"]) + logger.info("Next show is %s", show["name"]) next_start = self.showtz.localize( - datetime.datetime.strptime( - show["starts"], self.__show_datetime_format - ) + datetime.datetime.strptime( # noqa: DTZ007 + show["starts"], + self.__show_datetime_format, + ), ) logger.warning( - datetime.datetime.now(pytz.timezone("UTC")) - + datetime.timedelta(minutes=15) + "%s", + ( + datetime.datetime.now(pytz.timezone("UTC")) + + datetime.timedelta(minutes=15) + ), ) logger.warning(next_start) if next_start < datetime.datetime.now( - pytz.timezone("UTC") + pytz.timezone("UTC"), ) + datetime.timedelta(minutes=15): logger.info("Next show starts soon enough, using it") return show - return + return None return data["shows"]["current"] diff --git a/nowplaying/show/show.py b/nowplaying/show/show.py index 305bb203..e21fa50b 100644 --- a/nowplaying/show/show.py +++ b/nowplaying/show/show.py @@ -1,7 +1,10 @@ +"""Nowplaying Show model.""" + import datetime import logging import logging.handlers import uuid +from typing import Self import pytz @@ -9,17 +12,19 @@ DEFAULT_SHOW_URL = "https://www.rabe.ch" +_EXCEPTION_SHOW_ERROR_STARTTIME_NO_DATETIME = "starttime has to be a datatime object" +_EXCEPTION_SHOW_ERROR_ENDTIME_NO_DATETIME = "endtime has to be a datatime object" + class ShowError(Exception): """Show related exception.""" - pass - class Show: """Show object which has a start and end time and an optional URL.""" - def __init__(self): + def __init__(self: Self) -> None: + """Create Show.""" self.name = "" self.url = DEFAULT_SHOW_URL @@ -36,29 +41,34 @@ def __init__(self): # The show's end time, initially set to to now self.endtime = now - def set_name(self, name): + def set_name(self: Self, name: str) -> None: + """Set Show name.""" # The name of the show self.name = name - def set_url(self, url): + def set_url(self: Self, url: str) -> None: + """Set Show URL.""" # The URL of the show self.url = url - def set_starttime(self, starttime): + def set_starttime(self: Self, starttime: datetime.datetime) -> None: + """Set Show start time.""" if not isinstance(starttime, datetime.datetime): - raise ShowError("starttime has to be a datatime object") + raise ShowError(_EXCEPTION_SHOW_ERROR_STARTTIME_NO_DATETIME) # The show's start time as a datetime object self.starttime = starttime - def set_endtime(self, endtime): + def set_endtime(self: Self, endtime: datetime.datetime) -> None: + """Set Show end time.""" if not isinstance(endtime, datetime.datetime): - raise ShowError("endtime has to be a datatime object") + raise ShowError(_EXCEPTION_SHOW_ERROR_ENDTIME_NO_DATETIME) # The show's end time as a datetime object self.endtime = endtime - def __str__(self): + def __str__(self: Self) -> str: + """Stringify Show.""" return ( f"Show '{self.name}' ({self.uuid}), " f"start: '{self.starttime}', end: '{self.endtime}', url: {self.url}" diff --git a/nowplaying/track/handler.py b/nowplaying/track/handler.py index 9aab92e3..04bf19c9 100644 --- a/nowplaying/track/handler.py +++ b/nowplaying/track/handler.py @@ -1,13 +1,21 @@ """Track event handling subject of the observer.""" +from __future__ import annotations + import logging import logging.handlers +from typing import TYPE_CHECKING, Self + +if TYPE_CHECKING: # pragma: no cover + from .observers.base import TrackObserver + from .track import Track -from .observers.base import TrackObserver -from .track import Track logger = logging.getLogger(__name__) +_EXCEPTION_TRACK_HANDLER_ERROR_START = "Observer failed to start track" +_EXCEPTION_TRACK_HANDLER_ERROR_FINISH = "Observer failed to finish track" + class TrackEventHandler: """Inform all registered track-event observers about a track change. @@ -15,24 +23,24 @@ class TrackEventHandler: This is the subject of the classical observer pattern """ - def __init__(self): + def __init__(self: Self) -> None: """Initialize the track event handler.""" - self.__observers = [] + self.__observers: list[TrackObserver] = [] - def register_observer(self, observer: TrackObserver): + def register_observer(self: Self, observer: TrackObserver) -> None: """Register an observer to be informed about track changes.""" logger.info("Registering TrackObserver '%s'", observer.__class__.__name__) self.__observers.append(observer) - def remove_observer(self, observer: TrackObserver): + def remove_observer(self: Self, observer: TrackObserver) -> None: """Remove an observer from the list of observers.""" self.__observers.remove(observer) - def get_observers(self) -> list: + def get_observers(self: Self) -> list: """Return register observers to allow inspecting them.""" return self.__observers - def track_started(self, track: Track): + def track_started(self: Self, track: Track) -> None: """Inform all registered track-event observers about a track started event.""" logger.info( "Sending track-started event to %s observers: %s", @@ -42,15 +50,16 @@ def track_started(self, track: Track): for observer in self.__observers: logger.debug( - "Sending track-started event to observer %s", observer.__class__ + "Sending track-started event to observer %s", + observer.__class__, ) try: observer.track_started(track) - except Exception as error: - logger.exception(error) + except Exception: + logger.exception(_EXCEPTION_TRACK_HANDLER_ERROR_START) - def track_finished(self, track: Track): + def track_finished(self: Self, track: Track) -> None: """Inform all registered track-event observers about a track finished event.""" logger.info( "Sending track-finished event to %s observers: %s", @@ -60,10 +69,11 @@ def track_finished(self, track: Track): for observer in self.__observers: logger.debug( - "Sending track-finished event to observer %s", observer.__class__ + "Sending track-finished event to observer %s", + observer.__class__, ) try: observer.track_finished(track) - except Exception as error: - logger.exception(error) + except Exception: + logger.exception(_EXCEPTION_TRACK_HANDLER_ERROR_FINISH) diff --git a/nowplaying/track/observers/base.py b/nowplaying/track/observers/base.py index 87b0cea6..d524043c 100644 --- a/nowplaying/track/observers/base.py +++ b/nowplaying/track/observers/base.py @@ -1,6 +1,16 @@ +"""Abstract base for TrackObservers.""" + +from __future__ import annotations + from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Self, TypeVar + +if TYPE_CHECKING: # pragma: no cover + import configargparse # type: ignore[import-untyped] -import configargparse + from nowplaying.track.track import Track + +TTrackObserverOptions = TypeVar("TTrackObserverOptions", bound="TrackObserver.Options") class TrackObserver(ABC): @@ -9,18 +19,24 @@ class TrackObserver(ABC): name = "TrackObserver" class Options(ABC): + """Abstract base class for add TrackObserver.Options.""" + @classmethod @abstractmethod - def args(cls, args: configargparse.ArgParser) -> None: # pragma: no cover - pass - - def get_name(self): + def args( + cls: type[TTrackObserverOptions], + args: configargparse.ArgParser, + ) -> None: # pragma: no cover + """Get args for Options.""" + + def get_name(self: Self) -> str: + """Get name.""" return self.name @abstractmethod - def track_started(self, track): # pragma: no cover - pass + def track_started(self: Self, track: Track) -> None: # pragma: no cover + """Track started.""" @abstractmethod - def track_finished(self, track): # pragma: no cover - pass + def track_finished(self: Self, track: Track) -> None: # pragma: no cover + """Track finished.""" diff --git a/nowplaying/track/observers/dab_audio_companion.py b/nowplaying/track/observers/dab_audio_companion.py index ee6b5744..67994321 100644 --- a/nowplaying/track/observers/dab_audio_companion.py +++ b/nowplaying/track/observers/dab_audio_companion.py @@ -1,3 +1,4 @@ +# mypy: ignore-errors import logging import urllib from datetime import timedelta @@ -48,7 +49,7 @@ def __init__(self, options: Options): self.dls_enabled = self.last_frame_was_dl_plus = self._options.dl_plus logger.info( "DAB+ Audio Companion initialised with URL: %s, DLS+ enabled: %r" - % (self.base_url, self.dls_enabled) + % (self.base_url, self.dls_enabled), ) def track_started(self, track: Track): @@ -61,9 +62,9 @@ def track_started(self, track: Track): if track.get_duration() < timedelta(seconds=5): logger.info( - "Track is less than 5 seconds, not sending to DAB+ Audio Companion" + "Track is less than 5 seconds, not sending to DAB+ Audio Companion", ) - return + return None if not track.has_default_title() and not track.has_default_artist(): params["artist"] = track.artist @@ -71,7 +72,7 @@ def track_started(self, track: Track): self.last_frame_was_dl_plus = True elif self.last_frame_was_dl_plus: logger.info( - "Track has default info, using show instead. Sending DLS+ delete tags." + "Track has default info, using show instead. Sending DLS+ delete tags.", ) message = DLPlusMessage() # track.artist contains station name if no artist is set @@ -91,7 +92,7 @@ def track_started(self, track: Track): logger.info( f"DAB+ Audio Companion URL: {self.base_url} " - f"data: {params} is DL+: {self.last_frame_was_dl_plus}" + f"data: {params} is DL+: {self.last_frame_was_dl_plus}", ) resp = requests.post(self.base_url, params) @@ -104,7 +105,7 @@ def _track_started_plain(self, track): if track.has_default_title() and track.has_default_artist(): logger.info( - "%s: Track has default info, using show instead" % self.__class__ + "%s: Track has default info, using show instead" % self.__class__, ) title = track.show.name @@ -112,7 +113,7 @@ def _track_started_plain(self, track): # artist is an unicode string which we have to encode into UTF-8 # http://bugs.python.org/issue216716 song_string = urllib.parse.quote_plus( - f"{track.artist.encode('utf8')} - {title.encode('utf8')}" + f"{track.artist.encode('utf8')} - {title.encode('utf8')}", ) update_url = f"{self.base_url}?dls={song_string}" diff --git a/nowplaying/track/observers/icecast.py b/nowplaying/track/observers/icecast.py index c1231df2..07a0cac2 100644 --- a/nowplaying/track/observers/icecast.py +++ b/nowplaying/track/observers/icecast.py @@ -1,14 +1,25 @@ +"""Send PAD to icecast endpoints.""" + +from __future__ import annotations + import logging +from typing import TYPE_CHECKING, Self -import configargparse import requests -from ...util import parse_icecast_url -from ..track import Track -from .base import TrackObserver +from nowplaying.track.observers.base import TrackObserver +from nowplaying.util import parse_icecast_url + +if TYPE_CHECKING: # pragma: no cover + import configargparse # type: ignore[import-untyped] + + from nowplaying.track.observers.base import TTrackObserverOptions + from nowplaying.track.track import Track logger = logging.getLogger(__name__) +_NOWPLAYING_TRACK_EXEPTION = "request failed" + class IcecastTrackObserver(TrackObserver): """Update track metadata on an icecast mountpoint.""" @@ -19,18 +30,26 @@ class Options(TrackObserver.Options): """IcecastTrackObserver options.""" @classmethod - def args(cls, args: configargparse.ArgParser) -> None: - # TODO v3 remove this option + def args( + cls: type[TTrackObserverOptions], + args: configargparse.ArgParser, + ) -> None: + """Args for IcecastTrackObserver.""" + # TODO(hairmare): v3 remove this option + # https://github.com/radiorabe/nowplaying/issues/179 args.add_argument( "-m", "--icecast-base", - dest="icecastBase", + dest="icecast_base", help="Icecast base URL", default="http://icecast.example.org:8000/admin/", ) - # TODO v3 remove this option + # TODO(hairmare): v3 remove this option + # https://github.com/radiorabe/nowplaying/issues/179 args.add_argument( - "--icecast-password", dest="icecastPassword", help="Icecast Password" + "--icecast-password", + dest="icecast_password", + help="Icecast Password", ) args.add_argument( "-i", @@ -44,17 +63,20 @@ def args(cls, args: configargparse.ArgParser) -> None: ) def __init__( - self, + self: Self, url: str, username: str | None = None, password: str | None = None, mount: str | None = None, - ): - # TODO v3 remove optional args and only support parsed URLs + ) -> None: + """Create IcecastTrackObserver.Config.""" + # TODO(hairmare): v3 remove optional args and only support parsed URLs + # https://github.com/radiorabe/nowplaying/issues/179 (self.url, self.username, self.password, self.mount) = parse_icecast_url( - url + url, ) - # TODO v3 remove non URL usage of username, password, ... + # TODO(hairmare): v3 remove non URL usage of username, password, ... + # https://github.com/radiorabe/nowplaying/issues/179 if not self.username and username: # grab from args if not in URL logger.warning("deprecated use username from URL") @@ -76,13 +98,17 @@ def __init__( if not self.mount: raise ValueError("Missing required parameter mount for %s" % url) - def __init__(self, options: Options): + def __init__(self: Self, options: Options) -> None: + """Create IcecastTrackObserver.""" self.options = options - logger.info(f"Icecast URL: {self.options.url} mount: {self.options.mount}") + logger.info("Icecast URL: %s mount: %s", self.options.url, self.options.mount) - def track_started(self, track: Track): + def track_started(self: Self, track: Track) -> None: + """Track started.""" logger.info( - f"Updating Icecast Metadata for track: {track.artist} - {track.title}" + "Updating Icecast Metadata for track: %s - %s", + track.artist, + track.title, ) title = track.title @@ -101,15 +127,19 @@ def track_started(self, track: Track): try: requests.get( self.options.url, - auth=(self.options.username, self.options.password), + auth=(self.options.username, self.options.password), # type: ignore[arg-type] params=params, + timeout=60, ) - except requests.exceptions.RequestException as e: - logger.exception(e) + except requests.exceptions.RequestException: + logger.exception(_NOWPLAYING_TRACK_EXEPTION) logger.info( - f"Icecast Metadata updated on {self.options.url} with data: {params}" + "Icecast Metadata updated on %s with data: %s", + self.options.url, + params, ) - def track_finished(self, track): - return True + def track_finished(self: Self, _: Track) -> None: + """Track finished.""" + return diff --git a/nowplaying/track/observers/scrobbler.py b/nowplaying/track/observers/scrobbler.py index 080712ca..db0aab0f 100644 --- a/nowplaying/track/observers/scrobbler.py +++ b/nowplaying/track/observers/scrobbler.py @@ -1,3 +1,4 @@ +# mypy: ignore-errors # TODO scrobbling is not currently supported # remove the no cover pragma from this file if you support it again import calendar # pragma: no cover @@ -41,7 +42,7 @@ def track_started(self, track): logger.info( "AS now-playing notification for track: %s - %s" - % (track.artist, track.title) + % (track.artist, track.title), ) # scrobble to all networks diff --git a/nowplaying/track/observers/smc_ftp.py b/nowplaying/track/observers/smc_ftp.py index 28fee051..3da6c276 100644 --- a/nowplaying/track/observers/smc_ftp.py +++ b/nowplaying/track/observers/smc_ftp.py @@ -1,15 +1,26 @@ +"""Upload PAD to SMC.""" + +from __future__ import annotations + import logging from datetime import timedelta from ftplib import FTP_TLS from io import BytesIO +from typing import TYPE_CHECKING, Self + +from nowplaying.track.observers.base import TrackObserver + +if TYPE_CHECKING: # pragma: no cover + import configargparse # type: ignore[import-untyped] -import configargparse + from nowplaying.track.observers.base import TTrackObserverOptions + from nowplaying.track.track import Track -from ..track import Track -from .base import TrackObserver logger = logging.getLogger(__name__) +_NOWPLAYING_DAB_MAXLEN = 128 + class SmcFtpTrackObserver(TrackObserver): """Update track metadata for DLS and DL+ to the SMC FTP server.""" @@ -17,8 +28,14 @@ class SmcFtpTrackObserver(TrackObserver): name = "SMC FTP" class Options(TrackObserver.Options): # pragma: no coverage + """Options for SmcFtpTrackObserver.""" + @classmethod - def args(cls, args: configargparse.ArgParser): + def args( + cls: type[TTrackObserverOptions], + args: configargparse.ArgParser, + ) -> None: + """Create args.""" args.add_argument( "--dab-smc", help="Enable SMC FTP delivery", @@ -35,35 +52,44 @@ def args(cls, args: configargparse.ArgParser): help="Username for SMC FTP server", ) args.add_argument( - "--dab-smc-ftp-password", help="Password for SMC FTP server" + "--dab-smc-ftp-password", + help="Password for SMC FTP server", ) - def __init__(self, hostname: str, username: str, password: str) -> None: + def __init__(self: Self, hostname: str, username: str, password: str) -> None: + """Create SmcFtpTrackObserver.Config.""" self.hostname: str = hostname self.username: str = username self.password: str = password - def __init__(self, options: Options): + def __init__(self: Self, options: Options) -> None: + """Create SmcFtpTrackObserver.""" self._options = options - def track_started(self, track: Track): - logger.info(f"Updating DAB+ DLS for track {track.artist=} {track.title=}") + def track_started(self: Self, track: Track) -> None: + """Track started.""" + logger.info( + "Updating DAB+ DLS for track artist=%s title=%s", + track.artist, + track.title, + ) if track.get_duration() < timedelta(seconds=5): logger.info( - "Track is less than 5 seconds, not sending to SMC" - f"{track.artist=} {track.title=}" + "Track is less than 5 seconds, not sending to SMC artist=%s title=%s", + track.artist, + track.title, ) return dls, dlplus = _dls_from_track(track) # check for too long meta and shorten to just artist - if dls.getbuffer().nbytes > 128: # pragma: no cover - logger.warning(f"SMC DLS to long {dls.getvalue().decode('latin1')=}") + if dls.getbuffer().nbytes > _NOWPLAYING_DAB_MAXLEN: # pragma: no cover + logger.warning("SMC DLS to long %s", dls.getvalue().decode("latin1")) dls, dlplus = _dls_from_track(track, title=False) - ftp = FTP_TLS() + ftp = FTP_TLS() # noqa: S321 ftp.connect(self._options.hostname) ftp.sendcmd(f"USER {self._options.username}") ftp.sendcmd(f"PASS {self._options.password}") @@ -75,16 +101,18 @@ def track_started(self, track: Track): ftp.close() logger.info( - f"SMC FTP {self._options.hostname=} " - f"{dls.getvalue().decode('latin1')=} " - f"{dlplus.getvalue().decode('latin1')=}" + "SMC FTP hostname=%s dls=%s dlsplus=%", + self._options.hostname, + dls.getvalue().decode("latin1"), + dlplus.getvalue().decode("latin1"), ) - def track_finished(self, track): - return True + def track_finished(self: Self, _: Track) -> None: + """Track finished.""" + return -def _dls_from_track(track: Track, title=True) -> (BytesIO, BytesIO): +def _dls_from_track(track: Track, *, title: bool = True) -> tuple[BytesIO, BytesIO]: # track.artist contains station name if no artist is set dls = f"{track.artist} - {track.show.name}" if title else track.artist dlplus = "" diff --git a/nowplaying/track/observers/ticker.py b/nowplaying/track/observers/ticker.py index 46619cbf..5a857d4c 100644 --- a/nowplaying/track/observers/ticker.py +++ b/nowplaying/track/observers/ticker.py @@ -1,15 +1,24 @@ +"""TickerTrackObserver generates the songticker.xml file.""" + +from __future__ import annotations + import datetime import logging import uuid import warnings +from typing import TYPE_CHECKING, Self -import configargparse -import isodate -import lxml.builder +import isodate # type: ignore[import-untyped] +import lxml.builder # type: ignore[import-untyped] import lxml.etree import pytz -from .base import TrackObserver +from nowplaying.track.observers.base import TrackObserver, TTrackObserverOptions + +if TYPE_CHECKING: # pragma: no cover + import configargparse # type: ignore[import-untyped] + + from nowplaying.track.track import Track logger = logging.getLogger(__name__) @@ -28,7 +37,11 @@ class Options(TrackObserver.Options): """TickerTrackObserver options.""" @classmethod - def args(cls, args: configargparse.ArgParser) -> None: + def args( + cls: type[TTrackObserverOptions], + args: configargparse.ArgParser, + ) -> None: + """Build args.""" args.add_argument( "--xml-output", dest="tickerOutputFile", @@ -36,19 +49,25 @@ def args(cls, args: configargparse.ArgParser) -> None: default="/var/www/localhost/htdocs/songticker/0.9.3/current.xml", ) - def __init__(self, file_path: str): + def __init__(self: Self, file_path: str) -> None: + """Create TickerTrackObserver.Config.""" self.file_path = file_path - def __init__(self, options: Options): + def __init__(self: Self, options: Options) -> None: + """Create TickerTrackObserver.""" warnings.warn( "The XML ticker format will be replaced with a JSON variant in the future", PendingDeprecationWarning, + stacklevel=2, ) self.ticker_file_path = options.file_path - def track_started(self, track): + def track_started(self: Self, track: Track) -> None: + """Track started.""" logger.info( - f"Updating Ticker XML file for track: {track.artist} - {track.title}" + "Updating Ticker XML file for track: %s - %s", + track.artist, + track.title, ) try: tz = pytz.timezone("Europe/Zurich") @@ -59,11 +78,11 @@ def track_started(self, track): now = isodate.datetime_isoformat(datetime.datetime.now(tz)) - MAIN_NAMESPACE = "http://rabe.ch/schema/ticker.xsd" - XLINK_NAMESPACE = "http://www.w3.org/1999/xlink" - XLINK = "{%s}" % XLINK_NAMESPACE + MAIN_NAMESPACE = "http://rabe.ch/schema/ticker.xsd" # noqa: N806 + XLINK_NAMESPACE = "http://www.w3.org/1999/xlink" # noqa: N806 + XLINK = "{%s}" % XLINK_NAMESPACE # noqa: N806 - E = lxml.builder.ElementMaker( + E = lxml.builder.ElementMaker( # noqa: N806 namespace=MAIN_NAMESPACE, nsmap={None: MAIN_NAMESPACE, "xlink": XLINK_NAMESPACE}, ) @@ -80,10 +99,10 @@ def track_started(self, track): E.name(track.show.name), show_ref, E.startTime( - isodate.datetime_isoformat(track.show.starttime.astimezone(tz)) + isodate.datetime_isoformat(track.show.starttime.astimezone(tz)), ), E.endTime( - isodate.datetime_isoformat(track.show.endtime.astimezone(tz)) + isodate.datetime_isoformat(track.show.endtime.astimezone(tz)), ), id=track.show.uuid, ), @@ -103,5 +122,5 @@ def track_started(self, track): encoding="utf-8", ) - def track_finished(self, track): - return True + def track_finished(self: Self, _: Track) -> None: + """Track finished.""" diff --git a/nowplaying/track/track.py b/nowplaying/track/track.py index 4bce5c0e..4dc38958 100644 --- a/nowplaying/track/track.py +++ b/nowplaying/track/track.py @@ -1,31 +1,39 @@ +"""Nowplaying Track model.""" + import datetime import logging import logging.handlers import uuid +from typing import Self import pytz +from nowplaying.show.show import Show + logger = logging.getLogger(__name__) DEFAULT_ARTIST = "Radio Bern" DEFAULT_TITLE = "Livestream" +_EXCEPTION_TRACK_ERROR_NUMBER_NOT_INT = "track number has to be a positive integer" +_EXCEPTION_TRACK_ERROR_STARTTIME_NO_DATETIME = "starttime has to be a datatime object" +_EXCEPTION_TRACK_ERROR_ENDTIME_NO_DATETIME = "endtime has to be a datatime object" + class TrackError(Exception): """Track related exception.""" - pass - class Track: """Track object which has a start and end time and a related show.""" - def __init__(self): - self.artist = None + def __init__(self: Self) -> None: + """Create Track object.""" + self.artist = "" - self.title = None + self.title = "" - self.album = None + self.album = "" self.track = 1 @@ -41,61 +49,69 @@ def __init__(self): # The show's end time, initially set to to now self.endtime = now - def set_artist(self, artist): + def set_artist(self: Self, artist: str) -> None: + """Set Track artist.""" self.artist = artist - def set_title(self, title): + def set_title(self: Self, title: str) -> None: + """Set Track title.""" self.title = title - def set_album(self, album): + def set_album(self: Self, album: str) -> None: + """Set Track album.""" self.album = album - def set_track(self, track): + def set_track(self: Self, track: int) -> None: + """Set Track number.""" if track < 0: - raise TrackError("track number has to be a positive integer") + raise TrackError(_EXCEPTION_TRACK_ERROR_NUMBER_NOT_INT) self.track = track - def set_starttime(self, starttime): + def set_starttime(self: Self, starttime: datetime.datetime) -> None: + """Set Track start time.""" if not isinstance(starttime, datetime.datetime): - raise TrackError("starttime has to be a datatime object") + raise TrackError(_EXCEPTION_TRACK_ERROR_STARTTIME_NO_DATETIME) # The track's start time as a datetime object self.starttime = starttime - def set_endtime(self, endtime): + def set_endtime(self: Self, endtime: datetime.datetime) -> None: + """Set Track end time.""" if not isinstance(endtime, datetime.datetime): - raise TrackError("endtime has to be a datatime object") + raise TrackError(_EXCEPTION_TRACK_ERROR_ENDTIME_NO_DATETIME) # The track's end time as a datetime object self.endtime = endtime - def set_duration(self, seconds): + def set_duration(self: Self, seconds: int) -> None: + """Set Track duration.""" self.endtime = self.starttime + datetime.timedelta(seconds=int(seconds)) - def set_show(self, show): - # if not isinstance(show, show.Show): - # raise TrackError('show has to be a Show object') - - # The show which the track is related to + def set_show(self: Self, show: Show) -> None: + """Set Show for Track.""" self.show = show - def get_duration(self): + def get_duration(self: Self) -> datetime.timedelta: + """Get duration of Track.""" return self.endtime - self.starttime - def has_default_artist(self): + def has_default_artist(self: Self) -> bool: + """Return True if Track has default artist.""" if self.artist == DEFAULT_ARTIST: return True return False - def has_default_title(self): + def has_default_title(self: Self) -> bool: + """Return True if Track has default title.""" if self.title == DEFAULT_TITLE: return True return False - def __str__(self): + def __str__(self: Self) -> str: + """Stringify Track.""" return ( f"Track '{self.artist}' - '{self.title}', " f"start: '{self.starttime}', end: '{self.endtime}', uid: {self.uuid}" diff --git a/nowplaying/util.py b/nowplaying/util.py index c15cbf31..3c56e459 100644 --- a/nowplaying/util.py +++ b/nowplaying/util.py @@ -1,3 +1,7 @@ +"""Utils for nowplaying.""" + +from __future__ import annotations + import logging from urllib.parse import parse_qs, urlparse @@ -25,10 +29,14 @@ def parse_icecast_url( ('https://localhost:443/', None, None, None) Args: + ---- url (str): The Icecast URL to parse. + Returns: + ------- Tuple[str, Optional[str], Optional[str], Optional[str]]: The URL, username, password, and mountpoint. + """ parsed = urlparse(url) port = parsed.port or parsed.scheme == "https" and 443 or 80 @@ -39,5 +47,5 @@ def parse_icecast_url( try: mount = parse_qs(parsed.query)["mount"][0] except KeyError: - logger.warning("Missing mount parameter in URL %s" % url) + logger.warning("Missing mount parameter in URL %s", url) return (url, username, password, mount) diff --git a/poetry.lock b/poetry.lock index 15337548..3eea5323 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -13,13 +13,13 @@ files = [ [[package]] name = "anyio" -version = "4.2.0" +version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [package.dependencies] @@ -31,6 +31,25 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + [[package]] name = "autocommand" version = "2.2.2" @@ -56,17 +75,6 @@ files = [ [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backoff" -version = "2.2.1" -description = "Function decoration for backoff and retry" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - [[package]] name = "black" version = "24.2.0" @@ -345,63 +353,63 @@ yaml = ["PyYAML"] [[package]] name = "coverage" -version = "7.4.1" +version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, + {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, + {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, + {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, + {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, + {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, + {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, + {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, + {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, + {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, + {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, + {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, + {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, ] [package.extras] @@ -452,6 +460,22 @@ files = [ [package.dependencies] python-dateutil = ">=2.4" +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "ghp-import" version = "2.1.0" @@ -488,13 +512,13 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "griffe" -version = "0.40.1" +version = "0.41.3" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.40.1-py3-none-any.whl", hash = "sha256:5b8c023f366fe273e762131fe4bfd141ea56c09b3cb825aa92d06a82681cfd93"}, - {file = "griffe-0.40.1.tar.gz", hash = "sha256:66c48a62e2ce5784b6940e603300fcfb807b6f099b94e7f753f1841661fd5c7c"}, + {file = "griffe-0.41.3-py3-none-any.whl", hash = "sha256:27b4610f1ba6e5d039e9f0a2c97232e13463df75e53cb1833e0679f3377b9de2"}, + {file = "griffe-0.41.3.tar.gz", hash = "sha256:9edcfa9f57f4d9c5fcc6d5ce067c67a685b7101a21a7d11848ce0437368e474c"}, ] [package.dependencies] @@ -502,69 +526,69 @@ colorama = ">=0.4" [[package]] name = "grpcio" -version = "1.60.1" +version = "1.62.1" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.7" files = [ - {file = "grpcio-1.60.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:14e8f2c84c0832773fb3958240c69def72357bc11392571f87b2d7b91e0bb092"}, - {file = "grpcio-1.60.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:33aed0a431f5befeffd9d346b0fa44b2c01aa4aeae5ea5b2c03d3e25e0071216"}, - {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:fead980fbc68512dfd4e0c7b1f5754c2a8e5015a04dea454b9cada54a8423525"}, - {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:082081e6a36b6eb5cf0fd9a897fe777dbb3802176ffd08e3ec6567edd85bc104"}, - {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55ccb7db5a665079d68b5c7c86359ebd5ebf31a19bc1a91c982fd622f1e31ff2"}, - {file = "grpcio-1.60.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b54577032d4f235452f77a83169b6527bf4b77d73aeada97d45b2aaf1bf5ce0"}, - {file = "grpcio-1.60.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d142bcd604166417929b071cd396aa13c565749a4c840d6c702727a59d835eb"}, - {file = "grpcio-1.60.1-cp310-cp310-win32.whl", hash = "sha256:2a6087f234cb570008a6041c8ffd1b7d657b397fdd6d26e83d72283dae3527b1"}, - {file = "grpcio-1.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:f2212796593ad1d0235068c79836861f2201fc7137a99aa2fea7beeb3b101177"}, - {file = "grpcio-1.60.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:79ae0dc785504cb1e1788758c588c711f4e4a0195d70dff53db203c95a0bd303"}, - {file = "grpcio-1.60.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:4eec8b8c1c2c9b7125508ff7c89d5701bf933c99d3910e446ed531cd16ad5d87"}, - {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:8c9554ca8e26241dabe7951aa1fa03a1ba0856688ecd7e7bdbdd286ebc272e4c"}, - {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91422ba785a8e7a18725b1dc40fbd88f08a5bb4c7f1b3e8739cab24b04fa8a03"}, - {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba6209c96828711cb7c8fcb45ecef8c8859238baf15119daa1bef0f6c84bfe7"}, - {file = "grpcio-1.60.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c71be3f86d67d8d1311c6076a4ba3b75ba5703c0b856b4e691c9097f9b1e8bd2"}, - {file = "grpcio-1.60.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5ef6cfaf0d023c00002ba25d0751e5995fa0e4c9eec6cd263c30352662cbce"}, - {file = "grpcio-1.60.1-cp311-cp311-win32.whl", hash = "sha256:a09506eb48fa5493c58f946c46754ef22f3ec0df64f2b5149373ff31fb67f3dd"}, - {file = "grpcio-1.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:49c9b6a510e3ed8df5f6f4f3c34d7fbf2d2cae048ee90a45cd7415abab72912c"}, - {file = "grpcio-1.60.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:b58b855d0071575ea9c7bc0d84a06d2edfbfccec52e9657864386381a7ce1ae9"}, - {file = "grpcio-1.60.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:a731ac5cffc34dac62053e0da90f0c0b8560396a19f69d9703e88240c8f05858"}, - {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:cf77f8cf2a651fbd869fbdcb4a1931464189cd210abc4cfad357f1cacc8642a6"}, - {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c557e94e91a983e5b1e9c60076a8fd79fea1e7e06848eb2e48d0ccfb30f6e073"}, - {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:069fe2aeee02dfd2135d562d0663fe70fbb69d5eed6eb3389042a7e963b54de8"}, - {file = "grpcio-1.60.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb0af13433dbbd1c806e671d81ec75bd324af6ef75171fd7815ca3074fe32bfe"}, - {file = "grpcio-1.60.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2f44c32aef186bbba254129cea1df08a20be414144ac3bdf0e84b24e3f3b2e05"}, - {file = "grpcio-1.60.1-cp312-cp312-win32.whl", hash = "sha256:a212e5dea1a4182e40cd3e4067ee46be9d10418092ce3627475e995cca95de21"}, - {file = "grpcio-1.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:6e490fa5f7f5326222cb9f0b78f207a2b218a14edf39602e083d5f617354306f"}, - {file = "grpcio-1.60.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:4216e67ad9a4769117433814956031cb300f85edc855252a645a9a724b3b6594"}, - {file = "grpcio-1.60.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:73e14acd3d4247169955fae8fb103a2b900cfad21d0c35f0dcd0fdd54cd60367"}, - {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:6ecf21d20d02d1733e9c820fb5c114c749d888704a7ec824b545c12e78734d1c"}, - {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33bdea30dcfd4f87b045d404388469eb48a48c33a6195a043d116ed1b9a0196c"}, - {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b69e79d00f78c81eecfb38f4516080dc7f36a198b6b37b928f1c13b3c063e9"}, - {file = "grpcio-1.60.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:39aa848794b887120b1d35b1b994e445cc028ff602ef267f87c38122c1add50d"}, - {file = "grpcio-1.60.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72153a0d2e425f45b884540a61c6639436ddafa1829a42056aa5764b84108b8e"}, - {file = "grpcio-1.60.1-cp37-cp37m-win_amd64.whl", hash = "sha256:50d56280b482875d1f9128ce596e59031a226a8b84bec88cb2bf76c289f5d0de"}, - {file = "grpcio-1.60.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:6d140bdeb26cad8b93c1455fa00573c05592793c32053d6e0016ce05ba267549"}, - {file = "grpcio-1.60.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:bc808924470643b82b14fe121923c30ec211d8c693e747eba8a7414bc4351a23"}, - {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:70c83bb530572917be20c21f3b6be92cd86b9aecb44b0c18b1d3b2cc3ae47df0"}, - {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b106bc52e7f28170e624ba61cc7dc6829566e535a6ec68528f8e1afbed1c41f"}, - {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e980cd6db1088c144b92fe376747328d5554bc7960ce583ec7b7d81cd47287"}, - {file = "grpcio-1.60.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c5807e9152eff15f1d48f6b9ad3749196f79a4a050469d99eecb679be592acc"}, - {file = "grpcio-1.60.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1c3dc536b3ee124e8b24feb7533e5c70b9f2ef833e3b2e5513b2897fd46763a"}, - {file = "grpcio-1.60.1-cp38-cp38-win32.whl", hash = "sha256:d7404cebcdb11bb5bd40bf94131faf7e9a7c10a6c60358580fe83913f360f929"}, - {file = "grpcio-1.60.1-cp38-cp38-win_amd64.whl", hash = "sha256:c8754c75f55781515a3005063d9a05878b2cfb3cb7e41d5401ad0cf19de14872"}, - {file = "grpcio-1.60.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:0250a7a70b14000fa311de04b169cc7480be6c1a769b190769d347939d3232a8"}, - {file = "grpcio-1.60.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:660fc6b9c2a9ea3bb2a7e64ba878c98339abaf1811edca904ac85e9e662f1d73"}, - {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:76eaaba891083fcbe167aa0f03363311a9f12da975b025d30e94b93ac7a765fc"}, - {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d97c65ea7e097056f3d1ead77040ebc236feaf7f71489383d20f3b4c28412a"}, - {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb2a2911b028f01c8c64d126f6b632fcd8a9ac975aa1b3855766c94e4107180"}, - {file = "grpcio-1.60.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5a1ebbae7e2214f51b1f23b57bf98eeed2cf1ba84e4d523c48c36d5b2f8829ff"}, - {file = "grpcio-1.60.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a66f4d2a005bc78e61d805ed95dedfcb35efa84b7bba0403c6d60d13a3de2d6"}, - {file = "grpcio-1.60.1-cp39-cp39-win32.whl", hash = "sha256:8d488fbdbf04283f0d20742b64968d44825617aa6717b07c006168ed16488804"}, - {file = "grpcio-1.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b7199cd2a55e62e45bfb629a35b71fc2c0cb88f686a047f25b1112d3810904"}, - {file = "grpcio-1.60.1.tar.gz", hash = "sha256:dd1d3a8d1d2e50ad9b59e10aa7f07c7d1be2b367f3f2d33c5fade96ed5460962"}, + {file = "grpcio-1.62.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:179bee6f5ed7b5f618844f760b6acf7e910988de77a4f75b95bbfaa8106f3c1e"}, + {file = "grpcio-1.62.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:48611e4fa010e823ba2de8fd3f77c1322dd60cb0d180dc6630a7e157b205f7ea"}, + {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b2a0e71b0a2158aa4bce48be9f8f9eb45cbd17c78c7443616d00abbe2a509f6d"}, + {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbe80577c7880911d3ad65e5ecc997416c98f354efeba2f8d0f9112a67ed65a5"}, + {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f6c693d446964e3292425e1d16e21a97a48ba9172f2d0df9d7b640acb99243"}, + {file = "grpcio-1.62.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:77c339403db5a20ef4fed02e4d1a9a3d9866bf9c0afc77a42234677313ea22f3"}, + {file = "grpcio-1.62.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b5a4ea906db7dec694098435d84bf2854fe158eb3cd51e1107e571246d4d1d70"}, + {file = "grpcio-1.62.1-cp310-cp310-win32.whl", hash = "sha256:4187201a53f8561c015bc745b81a1b2d278967b8de35f3399b84b0695e281d5f"}, + {file = "grpcio-1.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:844d1f3fb11bd1ed362d3fdc495d0770cfab75761836193af166fee113421d66"}, + {file = "grpcio-1.62.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:833379943d1728a005e44103f17ecd73d058d37d95783eb8f0b28ddc1f54d7b2"}, + {file = "grpcio-1.62.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:c7fcc6a32e7b7b58f5a7d27530669337a5d587d4066060bcb9dee7a8c833dfb7"}, + {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:fa7d28eb4d50b7cbe75bb8b45ed0da9a1dc5b219a0af59449676a29c2eed9698"}, + {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48f7135c3de2f298b833be8b4ae20cafe37091634e91f61f5a7eb3d61ec6f660"}, + {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f11fd63365ade276c9d4a7b7df5c136f9030e3457107e1791b3737a9b9ed6a"}, + {file = "grpcio-1.62.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b49fd8fe9f9ac23b78437da94c54aa7e9996fbb220bac024a67469ce5d0825f"}, + {file = "grpcio-1.62.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:482ae2ae78679ba9ed5752099b32e5fe580443b4f798e1b71df412abf43375db"}, + {file = "grpcio-1.62.1-cp311-cp311-win32.whl", hash = "sha256:1faa02530b6c7426404372515fe5ddf66e199c2ee613f88f025c6f3bd816450c"}, + {file = "grpcio-1.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bd90b8c395f39bc82a5fb32a0173e220e3f401ff697840f4003e15b96d1befc"}, + {file = "grpcio-1.62.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:b134d5d71b4e0837fff574c00e49176051a1c532d26c052a1e43231f252d813b"}, + {file = "grpcio-1.62.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d1f6c96573dc09d50dbcbd91dbf71d5cf97640c9427c32584010fbbd4c0e0037"}, + {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:359f821d4578f80f41909b9ee9b76fb249a21035a061a327f91c953493782c31"}, + {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a485f0c2010c696be269184bdb5ae72781344cb4e60db976c59d84dd6354fac9"}, + {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b50b09b4dc01767163d67e1532f948264167cd27f49e9377e3556c3cba1268e1"}, + {file = "grpcio-1.62.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3227c667dccbe38f2c4d943238b887bac588d97c104815aecc62d2fd976e014b"}, + {file = "grpcio-1.62.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3952b581eb121324853ce2b191dae08badb75cd493cb4e0243368aa9e61cfd41"}, + {file = "grpcio-1.62.1-cp312-cp312-win32.whl", hash = "sha256:83a17b303425104d6329c10eb34bba186ffa67161e63fa6cdae7776ff76df73f"}, + {file = "grpcio-1.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:6696ffe440333a19d8d128e88d440f91fb92c75a80ce4b44d55800e656a3ef1d"}, + {file = "grpcio-1.62.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:e3393b0823f938253370ebef033c9fd23d27f3eae8eb9a8f6264900c7ea3fb5a"}, + {file = "grpcio-1.62.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:83e7ccb85a74beaeae2634f10eb858a0ed1a63081172649ff4261f929bacfd22"}, + {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:882020c87999d54667a284c7ddf065b359bd00251fcd70279ac486776dbf84ec"}, + {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a10383035e864f386fe096fed5c47d27a2bf7173c56a6e26cffaaa5a361addb1"}, + {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:960edebedc6b9ada1ef58e1c71156f28689978188cd8cff3b646b57288a927d9"}, + {file = "grpcio-1.62.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:23e2e04b83f347d0aadde0c9b616f4726c3d76db04b438fd3904b289a725267f"}, + {file = "grpcio-1.62.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978121758711916d34fe57c1f75b79cdfc73952f1481bb9583399331682d36f7"}, + {file = "grpcio-1.62.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9084086190cc6d628f282e5615f987288b95457292e969b9205e45b442276407"}, + {file = "grpcio-1.62.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:22bccdd7b23c420a27fd28540fb5dcbc97dc6be105f7698cb0e7d7a420d0e362"}, + {file = "grpcio-1.62.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:8999bf1b57172dbc7c3e4bb3c732658e918f5c333b2942243f10d0d653953ba9"}, + {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:d9e52558b8b8c2f4ac05ac86344a7417ccdd2b460a59616de49eb6933b07a0bd"}, + {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1714e7bc935780bc3de1b3fcbc7674209adf5208ff825799d579ffd6cd0bd505"}, + {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8842ccbd8c0e253c1f189088228f9b433f7a93b7196b9e5b6f87dba393f5d5d"}, + {file = "grpcio-1.62.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f1e7b36bdff50103af95a80923bf1853f6823dd62f2d2a2524b66ed74103e49"}, + {file = "grpcio-1.62.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bba97b8e8883a8038606480d6b6772289f4c907f6ba780fa1f7b7da7dfd76f06"}, + {file = "grpcio-1.62.1-cp38-cp38-win32.whl", hash = "sha256:a7f615270fe534548112a74e790cd9d4f5509d744dd718cd442bf016626c22e4"}, + {file = "grpcio-1.62.1-cp38-cp38-win_amd64.whl", hash = "sha256:e6c8c8693df718c5ecbc7babb12c69a4e3677fd11de8886f05ab22d4e6b1c43b"}, + {file = "grpcio-1.62.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:73db2dc1b201d20ab7083e7041946910bb991e7e9761a0394bbc3c2632326483"}, + {file = "grpcio-1.62.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:407b26b7f7bbd4f4751dbc9767a1f0716f9fe72d3d7e96bb3ccfc4aace07c8de"}, + {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f8de7c8cef9261a2d0a62edf2ccea3d741a523c6b8a6477a340a1f2e417658de"}, + {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd5c8a1af40ec305d001c60236308a67e25419003e9bb3ebfab5695a8d0b369"}, + {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0477cb31da67846a33b1a75c611f88bfbcd427fe17701b6317aefceee1b96f"}, + {file = "grpcio-1.62.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:60dcd824df166ba266ee0cfaf35a31406cd16ef602b49f5d4dfb21f014b0dedd"}, + {file = "grpcio-1.62.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:973c49086cabab773525f6077f95e5a993bfc03ba8fc32e32f2c279497780585"}, + {file = "grpcio-1.62.1-cp39-cp39-win32.whl", hash = "sha256:12859468e8918d3bd243d213cd6fd6ab07208195dc140763c00dfe901ce1e1b4"}, + {file = "grpcio-1.62.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7209117bbeebdfa5d898205cc55153a51285757902dd73c47de498ad4d11332"}, + {file = "grpcio-1.62.1.tar.gz", hash = "sha256:6c455e008fa86d9e9a9d85bb76da4277c0d7d9668a3bfa70dbe86e9f3c759947"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.60.1)"] +protobuf = ["grpcio-tools (>=1.62.1)"] [[package]] name = "h11" @@ -579,13 +603,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.4" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, ] [package.dependencies] @@ -596,17 +620,17 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.23.0)"] +trio = ["trio (>=0.22.0,<0.25.0)"] [[package]] name = "httpx" -version = "0.26.0" +version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, - {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, ] [package.dependencies] @@ -696,20 +720,6 @@ files = [ [package.dependencies] six = "*" -[[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 = "jaraco-collections" version = "5.0.0" @@ -893,6 +903,20 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.7)"] +[[package]] +name = "lxml-stubs" +version = "0.5.1" +description = "Type annotations for the lxml package" +optional = false +python-versions = "*" +files = [ + {file = "lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d"}, + {file = "lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272"}, +] + +[package.extras] +test = ["coverage[toml] (>=7.2.5)", "mypy (>=1.2.0)", "pytest (>=7.3.0)", "pytest-mypy-plugins (>=1.10.1)"] + [[package]] name = "markdown" version = "3.5.2" @@ -1118,13 +1142,13 @@ mkdocs = ">=1.2" [[package]] name = "mkdocstrings" -version = "0.24.0" +version = "0.24.1" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.24.0-py3-none-any.whl", hash = "sha256:f4908560c10f587326d8f5165d1908817b2e280bbf707607f601c996366a2264"}, - {file = "mkdocstrings-0.24.0.tar.gz", hash = "sha256:222b1165be41257b494a9d29b14135d2b7ca43f38161d5b10caae03b87bd4f7e"}, + {file = "mkdocstrings-0.24.1-py3-none-any.whl", hash = "sha256:b4206f9a2ca8a648e222d5a0ca1d36ba7dee53c88732818de183b536f9042b5d"}, + {file = "mkdocstrings-0.24.1.tar.gz", hash = "sha256:cc83f9a1c8724fc1be3c2fa071dd73d91ce902ef6a79710249ec8d0ee1064401"}, ] [package.dependencies] @@ -1171,67 +1195,67 @@ files = [ [[package]] name = "msgpack" -version = "1.0.7" +version = "1.0.8" description = "MessagePack serializer" optional = false python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, - {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, - {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, - {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, - {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, - {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, - {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, - {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, - {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, - {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, - {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, - {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [[package]] @@ -1245,6 +1269,52 @@ files = [ {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, ] +[[package]] +name = "mypy" +version = "1.9.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +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" @@ -1273,13 +1343,13 @@ pytz = ">=2022.7.1" [[package]] name = "opentelemetry-api" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Python API" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.22.0-py3-none-any.whl", hash = "sha256:43621514301a7e9f5d06dd8013a1b450f30c2e9372b8e30aaeb4562abf2ce034"}, - {file = "opentelemetry_api-1.22.0.tar.gz", hash = "sha256:15ae4ca925ecf9cfdfb7a709250846fbb08072260fca08ade78056c502b86bed"}, + {file = "opentelemetry_api-1.23.0-py3-none-any.whl", hash = "sha256:cc03ea4025353048aadb9c64919099663664672ea1c6be6ddd8fee8e4cd5e774"}, + {file = "opentelemetry_api-1.23.0.tar.gz", hash = "sha256:14a766548c8dd2eb4dfc349739eb4c3893712a0daa996e5dbf945f9da665da9d"}, ] [package.dependencies] @@ -1288,91 +1358,88 @@ importlib-metadata = ">=6.0,<7.0" [[package]] name = "opentelemetry-exporter-otlp" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Collector Exporters" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp-1.22.0-py3-none-any.whl", hash = "sha256:cb03a1cbf300e12b47690858be13dd26fe2f60b2610204959f3497cd6645e3a1"}, - {file = "opentelemetry_exporter_otlp-1.22.0.tar.gz", hash = "sha256:309a7d4dc67602801f15818e110ce452e78989886aaab5d37e7cf7f55f1d3d27"}, + {file = "opentelemetry_exporter_otlp-1.23.0-py3-none-any.whl", hash = "sha256:92371fdc8d7803465a45801fe30cd8c522ef355a385b0a1d5346d32f77511ea2"}, + {file = "opentelemetry_exporter_otlp-1.23.0.tar.gz", hash = "sha256:4af8798f9bc3bddb92fcbb5b4aa9d0e955d962aa1d9bceaab08891c355a9f907"}, ] [package.dependencies] -opentelemetry-exporter-otlp-proto-grpc = "1.22.0" -opentelemetry-exporter-otlp-proto-http = "1.22.0" +opentelemetry-exporter-otlp-proto-grpc = "1.23.0" +opentelemetry-exporter-otlp-proto-http = "1.23.0" [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Protobuf encoding" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.22.0-py3-none-any.whl", hash = "sha256:3f2538bec5312587f8676c332b3747f54c89fe6364803a807e217af4603201fa"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.22.0.tar.gz", hash = "sha256:71ae2f81bc6d6fe408d06388826edc8933759b2ca3a97d24054507dc7cfce52d"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.23.0-py3-none-any.whl", hash = "sha256:2a9e7e9d5a8b026b572684b6b24dcdefcaa58613d5ce3d644130b0c373c056c1"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.23.0.tar.gz", hash = "sha256:35e4ea909e7a0b24235bd0aaf17fba49676527feb1823b46565ff246d5a1ab18"}, ] [package.dependencies] -backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} -opentelemetry-proto = "1.22.0" +opentelemetry-proto = "1.23.0" [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.22.0-py3-none-any.whl", hash = "sha256:b5bcadc129272004316a455e9081216d3380c1fc2231a928ea6a70aa90e173fb"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.22.0.tar.gz", hash = "sha256:1e0e5aa4bbabc74942f06f268deffd94851d12a8dc30b02527472ef1729fe5b1"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.23.0-py3-none-any.whl", hash = "sha256:40f9e3e7761eb34f2a1001f4543028783ac26e2db27e420d5374f2cca0182dad"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.23.0.tar.gz", hash = "sha256:aa1a012eea5342bfef51fcf3f7f22601dcb0f0984a07ffe6025b2fbb6d91a2a9"}, ] [package.dependencies] -backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} deprecated = ">=1.2.6" googleapis-common-protos = ">=1.52,<2.0" grpcio = ">=1.0.0,<2.0.0" opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.22.0" -opentelemetry-proto = "1.22.0" -opentelemetry-sdk = ">=1.22.0,<1.23.0" +opentelemetry-exporter-otlp-proto-common = "1.23.0" +opentelemetry-proto = "1.23.0" +opentelemetry-sdk = ">=1.23.0,<1.24.0" [package.extras] test = ["pytest-grpc"] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Collector Protobuf over HTTP Exporter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.22.0-py3-none-any.whl", hash = "sha256:e002e842190af45b91dc55a97789d0b98e4308c88d886b16049ee90e17a4d396"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.22.0.tar.gz", hash = "sha256:79ed108981ec68d5f7985355bca32003c2f3a5be1534a96d62d5861b758a82f4"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.23.0-py3-none-any.whl", hash = "sha256:ad853b58681df8efcb2cfc93be2b5fd86351c99ff4ab47dc917da384b8650d91"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.23.0.tar.gz", hash = "sha256:088eac2320f4a604e2d9ff71aced71fdae601ac6457005fb0303d6bbbf44e6ca"}, ] [package.dependencies] -backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} deprecated = ">=1.2.6" googleapis-common-protos = ">=1.52,<2.0" opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.22.0" -opentelemetry-proto = "1.22.0" -opentelemetry-sdk = ">=1.22.0,<1.23.0" +opentelemetry-exporter-otlp-proto-common = "1.23.0" +opentelemetry-proto = "1.23.0" +opentelemetry-sdk = ">=1.23.0,<1.24.0" requests = ">=2.7,<3.0" [package.extras] -test = ["responses (==0.22.0)"] +test = ["responses (>=0.22.0,<0.25)"] [[package]] name = "opentelemetry-proto" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Python Proto" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_proto-1.22.0-py3-none-any.whl", hash = "sha256:ce7188d22c75b6d0fe53e7fb58501613d0feade5139538e79dedd9420610fa0c"}, - {file = "opentelemetry_proto-1.22.0.tar.gz", hash = "sha256:9ec29169286029f17ca34ec1f3455802ffb90131642d2f545ece9a63e8f69003"}, + {file = "opentelemetry_proto-1.23.0-py3-none-any.whl", hash = "sha256:4c017deca052cb287a6003b7c989ed8b47af65baeb5d57ebf93dde0793f78509"}, + {file = "opentelemetry_proto-1.23.0.tar.gz", hash = "sha256:e6aaf8b7ace8d021942d546161401b83eed90f9f2cc6f13275008cea730e4651"}, ] [package.dependencies] @@ -1380,40 +1447,40 @@ protobuf = ">=3.19,<5.0" [[package]] name = "opentelemetry-sdk" -version = "1.22.0" +version = "1.23.0" description = "OpenTelemetry Python SDK" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.22.0-py3-none-any.whl", hash = "sha256:a730555713d7c8931657612a88a141e3a4fe6eb5523d9e2d5a8b1e673d76efa6"}, - {file = "opentelemetry_sdk-1.22.0.tar.gz", hash = "sha256:45267ac1f38a431fc2eb5d6e0c0d83afc0b78de57ac345488aa58c28c17991d0"}, + {file = "opentelemetry_sdk-1.23.0-py3-none-any.whl", hash = "sha256:a93c96990ac0f07c6d679e2f1015864ff7a4f5587122dd5af968034436efb1fd"}, + {file = "opentelemetry_sdk-1.23.0.tar.gz", hash = "sha256:9ddf60195837b59e72fd2033d6a47e2b59a0f74f0ec37d89387d89e3da8cab7f"}, ] [package.dependencies] -opentelemetry-api = "1.22.0" -opentelemetry-semantic-conventions = "0.43b0" +opentelemetry-api = "1.23.0" +opentelemetry-semantic-conventions = "0.44b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.43b0" +version = "0.44b0" description = "OpenTelemetry Semantic Conventions" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.43b0-py3-none-any.whl", hash = "sha256:291284d7c1bf15fdaddf309b3bd6d3b7ce12a253cec6d27144439819a15d8445"}, - {file = "opentelemetry_semantic_conventions-0.43b0.tar.gz", hash = "sha256:b9576fb890df479626fa624e88dde42d3d60b8b6c8ae1152ad157a8b97358635"}, + {file = "opentelemetry_semantic_conventions-0.44b0-py3-none-any.whl", hash = "sha256:7c434546c9cbd797ab980cc88bf9ff3f4a5a28f941117cad21694e43d5d92019"}, + {file = "opentelemetry_semantic_conventions-0.44b0.tar.gz", hash = "sha256:2e997cb28cd4ca81a25a9a43365f593d0c2b76be0685015349a89abdf1aa4ffa"}, ] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1487,38 +1554,38 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", [[package]] name = "protobuf" -version = "4.25.2" +version = "4.25.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, - {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, - {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, - {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, - {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, - {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, - {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, - {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, - {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, ] [[package]] name = "pydantic" -version = "2.6.1" +version = "2.6.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, - {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.2" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -1526,112 +1593,95 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.2" +version = "2.16.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, - {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, - {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, - {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, - {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, - {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, - {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, - {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, - {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, - {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, - {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, - {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, - {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, - {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - [[package]] name = "pygments" version = "2.17.2" @@ -1666,13 +1716,13 @@ tests = ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"] [[package]] name = "pymdown-extensions" -version = "10.7" +version = "10.7.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"}, - {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"}, + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, ] [package.dependencies] @@ -1720,6 +1770,23 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-mypy" +version = "0.10.3" +description = "Mypy static type checker plugin for Pytest" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-mypy-0.10.3.tar.gz", hash = "sha256:f8458f642323f13a2ca3e2e61509f7767966b527b4d8adccd5032c3e7b4fd3db"}, + {file = "pytest_mypy-0.10.3-py3-none-any.whl", hash = "sha256:7638d0d3906848fc1810cb2f5cc7fceb4cc5c98524aafcac58f28620e3102053"}, +] + +[package.dependencies] +attrs = ">=19.0" +filelock = ">=3.0" +mypy = {version = ">=0.900", markers = "python_version >= \"3.11\""} +pytest = {version = ">=6.2", markers = "python_version >= \"3.10\""} + [[package]] name = "pytest-random-order" version = "1.1.1" @@ -1751,13 +1818,13 @@ ruff = ">=0.0.242" [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -1816,7 +1883,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2034,19 +2100,19 @@ files = [ [[package]] name = "setuptools" -version = "69.0.3" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -2061,35 +2127,24 @@ files = [ [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "tempora" -version = "5.5.0" +version = "5.5.1" description = "Objects and routines pertaining to date and time (tempora)" optional = false python-versions = ">=3.8" files = [ - {file = "tempora-5.5.0-py3-none-any.whl", hash = "sha256:defd976c9144fc9f20432ed0d75926c1c48dd0c7c701b3493e02d5316598e8fe"}, - {file = "tempora-5.5.0.tar.gz", hash = "sha256:13e4fcc997d0509c3306d6841f03e9381b7e5e46b2bebfae9151af90085f0c26"}, + {file = "tempora-5.5.1-py3-none-any.whl", hash = "sha256:33c1ef063c41d9ea14e7c2f809783241c982863e813d6a9cd138d1a48d796ce8"}, + {file = "tempora-5.5.1.tar.gz", hash = "sha256:a2bb51e2121976d931347b3e433917c364b83fdd5f64ef27336c865bf1fb0f75"}, ] [package.dependencies] @@ -2097,8 +2152,8 @@ files = [ pytz = "*" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["backports.unittest-mock", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-freezer", "pytest-mypy (>=0.9.1)", "pytest-ruff", "types-pytz"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-freezer", "pytest-mypy", "pytest-ruff (>=0.2.1)", "types-pytz"] [[package]] name = "text-unidecode" @@ -2111,15 +2166,40 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] +[[package]] +name = "types-pytz" +version = "2024.1.0.20240203" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.1.0.20240203.tar.gz", hash = "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"}, + {file = "types_pytz-2024.1.0.20240203-py3-none-any.whl", hash = "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3"}, +] + +[[package]] +name = "types-requests" +version = "2.31.0.20240310" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240310.tar.gz", hash = "sha256:8a20171e088a0f7893aac490a5f7f37e9f53e14d6b762c5d55623bea022ee06f"}, + {file = "types_requests-2.31.0.20240310-py3-none-any.whl", hash = "sha256:a4ba49353e3f6b3fdb481ba6f8455917e8bc2a0b62b775d80833ec000508e5dc"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -2135,13 +2215,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.0" +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.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, - {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] @@ -2322,4 +2402,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "2736e59e77b0dcdee9edc9b7213f1862200e1219711341ab68aa419d6095cc89" +content-hash = "1bd3678e5639d3792b7de59256ad07cae69f54ed87f922d4b3997c1fc3d37e83" diff --git a/pyproject.toml b/pyproject.toml index 31eb1a45..75207afa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ classifiers = [ ] readme = "README.md" packages = [ - { include = "nowplaying"}, + { include = "nowplaying" }, ] [tool.poetry.scripts] @@ -36,13 +36,6 @@ opentelemetry-sdk = "^1.18.0" [tool.poetry.group.dev.dependencies] black = ">=23.1,<25.0" -isort = "^5.12.0" -pydocstyle = "^6.1.1" -pytest = ">=7.2.3,<9.0.0" -pytest-cov = "^4.0.0" -pytest-random-order = "^1.1.0" -pytest-ruff = ">=0.1,<0.4" -ruff = ">=0.0.275,<0.3.3" faker = ">=18.11.2,<25.0.0" mkdocs = "^1.5.3" mkdocs-material = "^9.4.7" @@ -51,14 +44,19 @@ mkdocs-gen-files = "^0.5.0" mkdocs-literate-nav = "^0.6.1" mkdocs-section-index = "^0.3.8" mkdocstrings-python = "^1.7.3" - -[tool.isort] -line_length = 120 -profile = "black" +pytest = ">=7.2.3,<9.0.0" +pytest-cov = "^4.0.0" +pytest-mypy = "^0.10.3" +pytest-random-order = "^1.1.0" +pytest-ruff = ">=0.1,<0.4" +ruff = ">=0.0.275,<0.3.3" +types-requests = "^2.31.0.20240310" +types-pytz = "^2024.1.0.20240203" +lxml-stubs = "^0.5.1" [tool.pytest.ini_options] minversion = "7.2" -addopts = "-ra -q --random-order --doctest-glob='*.md' --doctest-modules --cov=nowplaying --cov-fail-under=100 --ruff --ignore docs/" +addopts = "-ra -q --random-order --doctest-glob='*.md' --doctest-modules --cov=nowplaying --cov-fail-under=100 --ruff --mypy --ignore docs/" filterwarnings = [ "ignore::DeprecationWarning:cairosvg", "ignore::DeprecationWarning:cherrypy", @@ -67,6 +65,13 @@ filterwarnings = [ "ignore::DeprecationWarning:pkg_resources", ] +[tool.ruff] +extend-exclude = [ + "nowplaying/misc/saemubox.py", + "nowplaying/track/observers/scrobbler.py", + "nowplaying/track/observers/dab_audio_companion.py", +] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..d7ff6e46 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,76 @@ +# [ruff](https://docs.astral.sh/ruff/) config +# +# templated with https://github.com/radiorabe/backstage-software-templates + +extend = "./pyproject.toml" + +[lint] +select = [ + "F", # pyflakes + "E", # pycodestyle errors + "I", # isort + "C90", # mccabe + "N", # pep8-naming + "D", # pydocstyle + "UP", # pyupgrade + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "S", # flake8-bandit + "BLE", # flake8-blind-exception + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SLOT", # flake8-slots + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "TD", # flake8-todos + "ERA", # eradicate + "PGH", # pygrep-hooks + "PL", # Pylint + "TRY", # tryceratops + "PERF", # Perflint + "RUF", # ruff specific rules +] +ignore = [ + "D203", # we prefer blank-line-before-class (D211) for black compat + "D213", # we prefer multi-line-summary-first-line (D212) +] + +[lint.per-file-ignores] +"tests/**/*.py" = [ + "D", # pydocstyle is optional for tests + "ANN", # flake8-annotations are optional for tests + "S101", # assert is allow in tests + "S108", # /tmp is allowed in tests since it's expected to be mocked + "DTZ001", # tests often run in UTC + "INP001", # tests do not need a dunder init +] +"**/__init__.py" = [ + "D104", # dunder init does not need a docstring because it might be empty +] +"docs/gen_ref_pages.py" = [ + "INP001", # mkdocs does not need a dunder init +] diff --git a/tests/conftest.py b/tests/conftest.py index de10fbb0..72488e8f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,9 @@ from werkzeug.wrappers import Response from nowplaying.api import ApiServer +from nowplaying.show.show import Show +from nowplaying.track.observers.base import TrackObserver +from nowplaying.track.track import Track class AuthenticatedClient(Client): @@ -45,19 +48,64 @@ def fixture_users(user, password): @pytest.fixture(name="options") def fixture_options(users): return SimpleNamespace( - **{ - "apiAuthUsers": users, - } + api_auth_users=users, ) @pytest.fixture(name="unauthenticated_client") def fixture_unauthenticated_client(options): """Create a test client.""" - yield Client(ApiServer(options, event_queue=Queue()), Response) + return Client(ApiServer(options, event_queue=Queue()), Response) @pytest.fixture(name="client") def fixture_client(options, user, password): """Create a test client.""" - yield AuthenticatedClient(ApiServer(options, event_queue=Queue()), user, password) + return AuthenticatedClient(ApiServer(options, event_queue=Queue()), user, password) + + +def new_show(name="Hairmare Traveling Medicine Show"): + s = Show() + s.set_name(name) + return s + + +@pytest.fixture() +def show_factory(): + """Return a method to help creating new show objects for tests.""" + return new_show + + +def new_track( + artist="Hairmare and the Band", + title="An Ode to legacy Python Code", + album="Live at the Refactoring Club", + duration=128, +): + t = Track() + t.set_artist(artist) + t.set_title(title) + t.set_album(album) + t.set_duration(duration) + return t + + +@pytest.fixture() +def track_factory(): + """Return a method to help creating new track objects for tests.""" + return new_track + + +class DummyObserver(TrackObserver): + """Shunt class for testing the abstract TrackObserver.""" + + def track_started(self, track): + pass + + def track_finished(self, track): + pass + + +@pytest.fixture() +def dummy_observer(): + return DummyObserver() diff --git a/tests/test_api.py b/tests/test_api.py index e53805d9..d99e5aec 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -21,18 +21,16 @@ def test_run_server_with_debug(mock_run_simple, users): mock_run_simple.side_effect = None options = SimpleNamespace( - **{ - "apiBindAddress": "0.0.0.0", - "apiPort": 8080, - "apiAuthUsers": users, - "debug": True, - } + api_bind_address="127.0.0.1", + api_port=8080, + api_auth_users=users, + debug=True, ) server = ApiServer(options, event_queue=Queue()) server.run_server() mock_run_simple.assert_called_once_with( - options.apiBindAddress, - options.apiPort, + options.api_bind_address, + options.api_port, mock.ANY, use_debugger=True, use_reloader=True, @@ -45,7 +43,7 @@ def test_stop_server(mock_server, mock_stop, options): """Test the stop_server function.""" api = ApiServer(options, event_queue=Queue()) - api._server = mock_server + api._server = mock_server # noqa: SLF001 api.stop_server() mock_server.stop.assert_called_once_with() @@ -59,7 +57,7 @@ def test_webhook_no_supported_header(client, content_type): if content_type: headers["Content-Type"] = content_type resp = client.post(_WEBHOOK_ENDPOINT, headers=headers, data="{}") - assert resp.status_code == 415 + assert resp.status_code == 415 # noqa: PLR2004 assert ( resp.data.decode("utf-8") == '"The server does not support the media type transmitted in the request."' @@ -67,18 +65,21 @@ def test_webhook_no_supported_header(client, content_type): @pytest.mark.parametrize( - "content_type", [_CONTENT_TYPE_JSON, _CONTENT_TYPE_CLOUDEVENTS] + "content_type", + [_CONTENT_TYPE_JSON, _CONTENT_TYPE_CLOUDEVENTS], ) def test_webhook_invalid_body(client, content_type): """Test the webhook function with invalid JSON.""" body = "invalid-json" resp = client.post( - _WEBHOOK_ENDPOINT, data=body, headers={"Content-Type": content_type} + _WEBHOOK_ENDPOINT, + data=body, + headers={"Content-Type": content_type}, ) - assert resp.status_code == 400 + assert resp.status_code == 400 # noqa: PLR2004 assert resp.data.decode("utf-8") == json.dumps( "Failed to read specversion from both headers and data. " - "The following can not be parsed as json: b'invalid-json'" + "The following can not be parsed as json: b'invalid-json'", ) @@ -91,19 +92,21 @@ def test_webhook_invalid_id_in_payload(client): "source": "https://rabe.ch", # a RaBe CRID must use "rabe.ch" so this is invalid "id": "crid://example.com/v1#t=code=19930301T131200.00Z", - } + }, ) resp = client.post( - _WEBHOOK_ENDPOINT, data=body, headers={"Content-Type": _CONTENT_TYPE_JSON} + _WEBHOOK_ENDPOINT, + data=body, + headers={"Content-Type": _CONTENT_TYPE_JSON}, ) - assert resp.status_code == 400 + assert resp.status_code == 400 # noqa: PLR2004 assert resp.data.decode("utf-8") == json.dumps( - "CRID 'crid://example.com/v1#t=code=19930301T131200.00Z' is not a RaBe CRID" + "CRID 'crid://example.com/v1#t=code=19930301T131200.00Z' is not a RaBe CRID", ) @pytest.mark.parametrize( - "content_type,body,expected_status", + ("content_type", "body", "expected_status"), [ ( _CONTENT_TYPE_JSON, @@ -125,14 +128,17 @@ def test_webhook_invalid_id_in_payload(client): def test_webhook_invalid_event(client, content_type, body, expected_status): """Test the webhook function.""" resp = client.post( - _WEBHOOK_ENDPOINT, data=json.dumps(body), headers={"Content-Type": content_type} + _WEBHOOK_ENDPOINT, + data=json.dumps(body), + headers={"Content-Type": content_type}, ) - assert resp.status_code == 400 + assert resp.status_code == 400 # noqa: PLR2004 assert expected_status in resp.data.decode("utf-8") @pytest.mark.parametrize( - "content_type", [_CONTENT_TYPE_JSON, _CONTENT_TYPE_CLOUDEVENTS] + "content_type", + [_CONTENT_TYPE_JSON, _CONTENT_TYPE_CLOUDEVENTS], ) def test_webhook_valid_event(client, content_type): """Test the webhook function.""" @@ -142,13 +148,15 @@ def test_webhook_valid_event(client, content_type): "type": "ch.rabe.api.events.track.v1.trackStarted", "source": "https://rabe.ch", "id": "crid://rabe.ch/v1#t=clock=19930301T131200.00Z", - } + }, ) assert client.application.event_queue.qsize() == 0 resp = client.post( - _WEBHOOK_ENDPOINT, data=body, headers={"Content-Type": content_type} + _WEBHOOK_ENDPOINT, + data=body, + headers={"Content-Type": content_type}, ) - assert resp.status_code == 200 + assert resp.status_code == 200 # noqa: PLR2004 assert resp.status == "200 Event Received" assert client.application.event_queue.qsize() == 1 event = client.application.event_queue.get() @@ -160,7 +168,9 @@ def test_webhook_valid_event(client, content_type): def test_webhook_auth_fail(unauthenticated_client): """Test the webhook function.""" resp = unauthenticated_client.post( - _WEBHOOK_ENDPOINT, data="{}", headers={"Content-Type": _CONTENT_TYPE_JSON} + _WEBHOOK_ENDPOINT, + data="{}", + headers={"Content-Type": _CONTENT_TYPE_JSON}, ) - assert resp.status_code == 401 + assert resp.status_code == 401 # noqa: PLR2004 assert resp.status == "401 UNAUTHORIZED" diff --git a/tests/test_daemon.py b/tests/test_daemon.py index e2118eb8..35fb7e92 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -35,10 +35,10 @@ def test_signal_handler(mock_sys_exit, options): with patch.object(SaemuBox, "__init__", lambda *_: None): nowplaying_daemon = NowPlayingDaemon(options) - nowplaying_daemon._api = Mock() + nowplaying_daemon._api = Mock() # noqa: SLF001 nowplaying_daemon.signal_handler(SIGINT, None) - nowplaying_daemon._api.stop_server.assert_called_once() + nowplaying_daemon._api.stop_server.assert_called_once() # noqa: SLF001 mock_sys_exit.assert_called_with(EX_OK) @@ -49,6 +49,6 @@ def test__start_apiserver(mock_run_server, options): with patch.object(SaemuBox, "__init__", lambda *_: None): daemon = NowPlayingDaemon(options) - daemon._start_apiserver() + daemon._start_apiserver() # noqa: SLF001 mock_run_server.assert_called_with() diff --git a/tests/test_input_handler.py b/tests/test_input_handler.py index cfbcdb07..72547ed8 100644 --- a/tests/test_input_handler.py +++ b/tests/test_input_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from queue import Queue from cloudevents.http.event import CloudEvent @@ -12,20 +14,20 @@ def __init__(self): self.event_queue = Queue() self.update_call = None - def update(self, saemubox_id: int, event: CloudEvent = None): + def update(self, saemubox_id: int, event: CloudEvent | None = None): self.update_call = (saemubox_id, event) - def handles(self, event: CloudEvent) -> bool: - return super().handles(event) + def handles(self, event: CloudEvent) -> bool: # noqa: ARG002 + return True def event(self, event: CloudEvent): - return super().event(event) + pass - def handle_id(self, saemubox_id: int, event: CloudEvent = None): - return super().handle_id(saemubox_id, event=event) + def handle_id(self, saemubox_id: int, event: CloudEvent | None = None): + pass - def handle(self, event: CloudEvent = None): - return super().handle(event) + def handle(self, event: CloudEvent | None = None): + pass def test_register_observer(): @@ -33,7 +35,7 @@ def test_register_observer(): handler = InputHandler() observer = ShuntInputObserver() handler.register_observer(observer) - assert observer in handler._observers + assert observer in handler._observers # noqa: SLF001 def test_remove_observer(): @@ -42,7 +44,7 @@ def test_remove_observer(): observer = ShuntInputObserver() handler.register_observer(observer) handler.remove_observer(observer) - assert observer not in handler._observers + assert observer not in handler._observers # noqa: SLF001 def test_update(): diff --git a/tests/test_input_observer.py b/tests/test_input_observer.py index 8b02f417..875df8a4 100644 --- a/tests/test_input_observer.py +++ b/tests/test_input_observer.py @@ -1,13 +1,13 @@ import pytest from cloudevents.http.event import CloudEvent -from isodate import parse_datetime +from isodate import parse_datetime # type: ignore[import-untyped] from nowplaying.input.observer import KlangbeckenInputObserver from nowplaying.track.handler import TrackEventHandler @pytest.mark.parametrize( - "source, expected", + ("source", "expected"), [ ("https://github/radiorabe/klangbecken", True), ("https://github/radiorabe/something-that-is-not-klangbecken", False), @@ -42,7 +42,6 @@ def test_klangbecken_input_observer_event(): data={"item.artist": "artist", "item.title": "title"}, ) observer.event(event) - # TODO assert something def test_klangbecken_input_observer_parse_event(): diff --git a/tests/test_input_observer_base.py b/tests/test_input_observer_base.py index 44899d52..54e8bac8 100644 --- a/tests/test_input_observer_base.py +++ b/tests/test_input_observer_base.py @@ -1,21 +1,29 @@ -from cloudevents.http.event import CloudEvent +from __future__ import annotations + +from typing import TYPE_CHECKING from nowplaying.input.observer import InputObserver +if TYPE_CHECKING: + from cloudevents.http.event import CloudEvent + class ShuntObserver(InputObserver): - def handles(self, event: CloudEvent): + def handles(self, _: CloudEvent): return True def event(self, event: CloudEvent): return super().event(event) - def handle_id(self, saemubox_id: int, event: CloudEvent): + def handle_id( + self, + saemubox_id: int, # noqa: ARG002 + event: CloudEvent | None = None, # noqa: ARG002 + ) -> bool: return True - def handle(self, event: CloudEvent): + def handle(self, event: CloudEvent | None = None) -> None: # noqa: ARG002 self.handle_called = True - return super().event(event) def test_init(): diff --git a/tests/test_input_observer_klangbecken.py b/tests/test_input_observer_klangbecken.py index ae420ad8..8134f611 100644 --- a/tests/test_input_observer_klangbecken.py +++ b/tests/test_input_observer_klangbecken.py @@ -31,7 +31,8 @@ def test_init(): @patch("nowplaying.show.client.ShowClient.get_show_info") @pytest.mark.parametrize( - "saemubox_id,expected", [(1, True), (2, False), (0, False), (-1, False)] + ("saemubox_id", "expected"), + [(1, True), (2, False), (0, False), (-1, False)], ) def test_handle_id(mock_get_show_info, saemubox_id, expected, event: CloudEvent): show_url = "http://www.rabe.ch/klangbecken/" @@ -65,11 +66,12 @@ def test_handle(mock_get_show_info): def test_parse_event(event: CloudEvent): expected_track = Track() - expected_track.artist = "Peaches" - expected_track.title = "Fuck the Pain Away" + expected_track.set_artist("Peaches") + expected_track.set_title("Fuck the Pain Away") observer = KlangbeckenInputObserver( - "http://example.org/klangbecken/", "tests/fixtures/now-playing.xml" + "http://example.org/klangbecken/", + "tests/fixtures/now-playing.xml", ) track = observer.parse_event(event) diff --git a/tests/test_main.py b/tests/test_main.py index f3e3672a..cbe27a0c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -23,7 +23,7 @@ def test_run( now_playing.run() mock_setup_otel.assert_called_once() - mock_setdefaulttimeout.assert_called_once_with(Options.socketDefaultTimeout) + mock_setdefaulttimeout.assert_called_once_with(Options.socket_default_timeout) mock_run_daemon.assert_called_once() @@ -36,7 +36,7 @@ def test_run_daemon(mock_daemon): now_playing = NowPlaying() now_playing.options = options - now_playing._run_daemon() + now_playing._run_daemon() # noqa: SLF001 mock_daemon.assert_called_once_with(options) mock_run.main.assert_called_once() diff --git a/tests/test_show_client.py b/tests/test_show_client.py index c6467189..26f143ae 100644 --- a/tests/test_show_client.py +++ b/tests/test_show_client.py @@ -2,6 +2,7 @@ import json from datetime import datetime, timedelta +from pathlib import Path from unittest.mock import Mock, patch import pytest @@ -16,7 +17,7 @@ def file_get_contents(filename: str) -> str: """Read a file and returns its contents.""" - with open(filename) as file: + with Path(filename).open() as file: return file.read() @@ -73,7 +74,7 @@ def test_lazy_update_with_show_set(mock_logger_debug): show_client.lazy_update() show_client.update.assert_not_called() mock_logger_debug.assert_called_once_with( - "Show still running, won't update show info" + "Show still running, won't update show info", ) @@ -82,17 +83,25 @@ def test_update(mock_requests_get): """Test :class:`ShowClient`'s :meth:`update` method.""" mock_requests_get.return_value.json = Mock( return_value=json.loads( - file_get_contents("tests/fixtures/cast_now_during_show.json") - ) + file_get_contents("tests/fixtures/cast_now_during_show.json"), + ), ) show_client = ShowClient(_BASE_URL) show_client.update() assert show_client.show.name == "Voice of Hindu Kush" assert show_client.show.starttime == datetime( - 2019, 1, 27, 13, tzinfo=pytz.timezone("UTC") + 2019, + 1, + 27, + 13, + tzinfo=pytz.timezone("UTC"), ) assert show_client.show.endtime == datetime( - 2319, 1, 27, 14, tzinfo=pytz.timezone("UTC") + 2319, + 1, + 27, + 14, + tzinfo=pytz.timezone("UTC"), ) assert show_client.show.url == "https://www.rabe.ch/stimme-der-kutuesch/" @@ -115,8 +124,8 @@ def test_update_no_url(mock_requests_get): """Test :class:`ShowClient`'s :meth:`update` method when no url is returned.""" mock_requests_get.return_value.json = Mock( return_value=json.loads( - file_get_contents("tests/fixtures/cast_now_no_url.json") - ) + file_get_contents("tests/fixtures/cast_now_no_url.json"), + ), ) show_client = ShowClient(_BASE_URL) show_client.update() @@ -125,7 +134,7 @@ def test_update_no_url(mock_requests_get): @patch("requests.get") @pytest.mark.parametrize( - "fixture,field", + ("fixture", "field"), [ ("cast_now_no_name", "name"), ("cast_now_no_end", "end time"), @@ -135,7 +144,7 @@ def test_update_no_url(mock_requests_get): def test_update_empty_field(mock_requests_get, fixture, field): """Test :class:`ShowClient`'s :meth:`update` method when a field is empty.""" mock_requests_get.return_value.json = Mock( - return_value=json.loads(file_get_contents(f"tests/fixtures/{fixture}.json")) + return_value=json.loads(file_get_contents(f"tests/fixtures/{fixture}.json")), ) show_client = ShowClient(_BASE_URL) with pytest.raises(ShowClientError) as info: @@ -148,8 +157,8 @@ def test_update_past_show(mock_requests_get): """Test :class:`ShowClient`'s :meth:`update` method when the show is in the past.""" mock_requests_get.return_value.json = Mock( return_value=json.loads( - file_get_contents("tests/fixtures/cast_now_past_show.json") - ) + file_get_contents("tests/fixtures/cast_now_past_show.json"), + ), ) show_client = ShowClient(_BASE_URL) with pytest.raises(ShowClientError) as info: @@ -165,8 +174,8 @@ def test_update_show_empty(mock_requests_get): """ mock_requests_get.return_value.json = Mock( return_value=json.loads( - file_get_contents("tests/fixtures/cast_now_show_empty.json") - ) + file_get_contents("tests/fixtures/cast_now_show_empty.json"), + ), ) show_client = ShowClient(_BASE_URL) show_client.update() @@ -179,8 +188,8 @@ def test_update_show_encoding_fix_in_name(mock_requests_get): """Test :class:`ShowClient`'s :meth:`update` for show name with encoding fix.""" mock_requests_get.return_value.json = Mock( return_value=json.loads( - file_get_contents("tests/fixtures/cast_now_show_encoding_fix.json") - ) + file_get_contents("tests/fixtures/cast_now_show_encoding_fix.json"), + ), ) show_client = ShowClient(_BASE_URL) show_client.update() @@ -192,16 +201,24 @@ def test_update_when_show_is_in_next_array(mock_requests_get): """Test :class:`ShowClient`'s :meth:`update` method.""" mock_requests_get.return_value.json = Mock( return_value=json.loads( - file_get_contents("tests/fixtures/cast_now_show_in_next.json") - ) + file_get_contents("tests/fixtures/cast_now_show_in_next.json"), + ), ) show_client = ShowClient(_BASE_URL) show_client.update() assert show_client.show.name == "Voice of Hindu Kush" assert show_client.show.starttime == datetime( - 2019, 1, 27, 13, tzinfo=pytz.timezone("UTC") + 2019, + 1, + 27, + 13, + tzinfo=pytz.timezone("UTC"), ) assert show_client.show.endtime == datetime( - 2319, 1, 27, 14, tzinfo=pytz.timezone("UTC") + 2319, + 1, + 27, + 14, + tzinfo=pytz.timezone("UTC"), ) assert show_client.show.url == "https://www.rabe.ch/stimme-der-kutuesch/" diff --git a/tests/test_track.py b/tests/test_track.py index 68fc13fe..79a89b7d 100644 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -18,7 +18,7 @@ def test_init(): def test_artist(): """Test :class:`Track`'s :meth:`artist` property.""" track = Track() - assert track.artist is None + assert track.artist == "" assert not track.has_default_artist() track.set_artist("Test") assert track.artist == "Test" @@ -30,7 +30,7 @@ def test_artist(): def test_title(): """Test :class:`Track`'s :meth:`title` property.""" track = Track() - assert track.title is None + assert track.title == "" assert not track.has_default_title() track.set_title("Test Title") assert track.title == "Test Title" @@ -42,7 +42,7 @@ def test_title(): def test_album(): """Test :class:`Track`'s :meth:`album` property.""" track = Track() - assert track.album is None + assert track.album == "" track.set_album("Test Album") assert track.album == "Test Album" @@ -52,7 +52,7 @@ def test_track(): track = Track() assert track.track == 1 track.set_track(2) - assert track.track == 2 + assert track.track == 2 # noqa: PLR2004 with pytest.raises(TypeError): track.set_track("no strings allowed") with pytest.raises(TrackError): @@ -102,4 +102,4 @@ def test_duration(): def test_prettyprinting(): """Test :class:`Track`'s :meth:`__str__` method.""" track = Track() - assert "Track 'None'" in str(track) + assert "Track ''" in str(track) diff --git a/tests/test_track_observer_dabaudiocompanion.py b/tests/test_track_observer_dabaudiocompanion.py index 809a7834..7be429ed 100644 --- a/tests/test_track_observer_dabaudiocompanion.py +++ b/tests/test_track_observer_dabaudiocompanion.py @@ -1,5 +1,8 @@ """Tests for :class:`DabAudioCompanionTrackObserver`.""" +# TODO(hairmare): v3 drop support +# https://github.com/radiorabe/nowplaying/issues/179 + from unittest.mock import MagicMock, Mock, patch from nowplaying.track.observers.dab_audio_companion import ( @@ -15,7 +18,7 @@ def test_init(): dab_audio_companion_track_observer = DabAudioCompanionTrackObserver( options=DabAudioCompanionTrackObserver.Options( url=_BASE_URL, - ) + ), ) assert dab_audio_companion_track_observer.base_url == f"{_BASE_URL}/api/setDLS" @@ -25,8 +28,7 @@ def test_track_started(mock_requests_post, track_factory, show_factory): """Test :class:`DabAudioCompanionTrackObserver`'s :meth:`track_started` method.""" mock_requests_post.return_value.getcode = Mock(return_value=200) mock_requests_post.return_value.read = Mock( - # TODO: mock and test real return value - return_value="contents" + return_value="contents", ) track = track_factory() @@ -35,7 +37,7 @@ def test_track_started(mock_requests_post, track_factory, show_factory): dab_audio_companion_track_observer = DabAudioCompanionTrackObserver( options=DabAudioCompanionTrackObserver.Options( url=_BASE_URL, - ) + ), ) # assume that last frame was DL+ on startup so we always send # delete tags when a show w/o dl+ starts @@ -73,7 +75,7 @@ def test_track_started(mock_requests_post, track_factory, show_factory): ] expected.sort() results.sort() - assert all([a == b for a, b in zip(results, expected)]) + assert all([a == b for a, b in zip(results, expected)]) # noqa: C419 # once ITEM delete have been sent we send regular DLS again dab_audio_companion_track_observer.track_started(track) @@ -92,10 +94,8 @@ def test_track_started(mock_requests_post, track_factory, show_factory): @patch("urllib.request.urlopen") def test_track_started_plain(mock_urlopen, track_factory, show_factory): - # TODO v3 remove when we drop plain support cm = MagicMock() cm.getcode.return_value = 200 - # TODO: mock and test real return value cm.read.return_value = "contents" cm.__enter__.return_value = cm mock_urlopen.return_value = cm @@ -107,7 +107,7 @@ def test_track_started_plain(mock_urlopen, track_factory, show_factory): options=DabAudioCompanionTrackObserver.Options( url=_BASE_URL, dl_plus=False, - ) + ), ) # last frame cannot be dl+ since the feature is inactive assert not o.last_frame_was_dl_plus @@ -115,7 +115,7 @@ def test_track_started_plain(mock_urlopen, track_factory, show_factory): o.track_started(track) assert not o.last_frame_was_dl_plus mock_urlopen.assert_called_with( - "http://localhost:80/api/setDLS?dls=b%27Hairmare+and+the+Band%27+-+b%27An+Ode+to+legacy+Python+Code%27" + "http://localhost:80/api/setDLS?dls=b%27Hairmare+and+the+Band%27+-+b%27An+Ode+to+legacy+Python+Code%27", ) track = track_factory(artist="Radio Bern", title="Livestream") @@ -123,7 +123,7 @@ def test_track_started_plain(mock_urlopen, track_factory, show_factory): o.track_started(track) mock_urlopen.assert_called_with( - "http://localhost:80/api/setDLS?dls=b%27Radio+Bern%27+-+b%27Hairmare+Traveling+Medicine+Show%27" + "http://localhost:80/api/setDLS?dls=b%27Radio+Bern%27+-+b%27Hairmare+Traveling+Medicine+Show%27", ) @@ -132,6 +132,6 @@ def test_track_finished(): dab_audio_companion_track_observer = DabAudioCompanionTrackObserver( options=DabAudioCompanionTrackObserver.Options( url=_BASE_URL, - ) + ), ) assert dab_audio_companion_track_observer.track_finished(Track()) diff --git a/tests/test_track_observer_icecast.py b/tests/test_track_observer_icecast.py index 58adf637..b148730b 100644 --- a/tests/test_track_observer_icecast.py +++ b/tests/test_track_observer_icecast.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( - "kwargs,url,username,password,mount", + ("kwargs", "url", "username", "password", "mount"), [ ( {"url": "http://user:password@localhost:80/?mount=foo.mp3"}, @@ -57,8 +57,8 @@ def test_init(): options=IcecastTrackObserver.Options( url="http://localhost:80/?mount=foo.mp3", username="foo", - password="bar", - ) + password="bar", # noqa: S106 + ), ) assert icecast_track_observer.options.url == "http://localhost:80/" assert icecast_track_observer.options.mount == "foo.mp3" @@ -67,23 +67,23 @@ def test_init(): options=IcecastTrackObserver.Options( url="http://localhost:80/", username="foo", - password="bar", + password="bar", # noqa: S106 mount="foo.mp3", - ) + ), ) assert icecast_track_observer.options.url == "http://localhost:80/" assert icecast_track_observer.options.mount == "foo.mp3" # test for exception if mount is missing - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Missing required parameter mount"): IcecastTrackObserver.Options( url="http://localhost:80/", username="foo", - password="bar", + password="bar", # noqa: S106 ) # test for exception if password is missing - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Missing required parameter password "): IcecastTrackObserver.Options( url="http://localhost:80/?mount=foo.mp3", username="foo", @@ -95,7 +95,6 @@ def test_track_started(mock_requests_get, track_factory, show_factory): """Test :class:`IcecastTrackObserver`'s :meth:`track_started` method.""" mock_resp = MagicMock() mock_resp.getcode.return_value = 200 - # TODO: mock and test real return value mock_resp.read.return_value = "contents" mock_resp.__enter__.return_value = mock_resp mock_requests_get.return_value = mock_resp @@ -107,8 +106,8 @@ def test_track_started(mock_requests_get, track_factory, show_factory): options=IcecastTrackObserver.Options( url="http://localhost:80/?mount=foo.mp3", username="foo", - password="bar", - ) + password="bar", # noqa: S106 + ), ) icecast_track_observer.track_started(track) @@ -121,6 +120,7 @@ def test_track_started(mock_requests_get, track_factory, show_factory): "charset": "utf-8", "song": "Hairmare and the Band - An Ode to legacy Python Code", }, + timeout=60, ) track = track_factory(artist="Radio Bern", title="Livestream") track.show = show_factory() @@ -135,6 +135,7 @@ def test_track_started(mock_requests_get, track_factory, show_factory): "charset": "utf-8", "song": "Radio Bern - Hairmare Traveling Medicine Show", }, + timeout=60, ) # test for ignoring of failed requests @@ -150,6 +151,7 @@ def test_track_started(mock_requests_get, track_factory, show_factory): "charset": "utf-8", "song": "Radio Bern - Hairmare Traveling Medicine Show", }, + timeout=60, ) @@ -159,7 +161,7 @@ def test_track_finished(): options=IcecastTrackObserver.Options( url="http://localhost:80/?mount=foo.mp3", username="foo", - password="bar", - ) + password="bar", # noqa: S106 + ), ) - assert icecast_track_observer.track_finished(Track()) + icecast_track_observer.track_finished(Track()) diff --git a/tests/test_track_observer_smc_ftp.py b/tests/test_track_observer_smc_ftp.py index 1c172911..432ec144 100644 --- a/tests/test_track_observer_smc_ftp.py +++ b/tests/test_track_observer_smc_ftp.py @@ -12,8 +12,8 @@ def test_init(): options=SmcFtpTrackObserver.Options( hostname="hostname", username="username", - password="password", - ) + password="password", # noqa: S106 + ), ) @@ -30,10 +30,10 @@ def test_track_started(mock_ftp, track_factory, show_factory): options=SmcFtpTrackObserver.Options( hostname="hostname", username="username", - password="password", - ) + password="password", # noqa: S106 + ), ) - smc_ftp_track_observer._ftp_cls = mock_ftp + smc_ftp_track_observer._ftp_cls = mock_ftp # noqa: SLF001 smc_ftp_track_observer.track_started(track) mock_ftp.assert_called_once() mock_ftp_instance.assert_has_calls( @@ -51,7 +51,7 @@ def test_track_started(mock_ftp, track_factory, show_factory): ), call.quit(), call.close(), - ] + ], ) # test skipping short tracks @@ -79,7 +79,7 @@ def test_track_started(mock_ftp, track_factory, show_factory): call.storlines("STOR /dlplus/nowplaying.dls", ANY), call.quit(), call.close(), - ] + ], ) @@ -89,7 +89,7 @@ def test_track_finished(): options=SmcFtpTrackObserver.Options( hostname="hostname", username="username", - password="password", - ) + password="password", # noqa: S106 + ), ) - assert smc_ftp_track_observer.track_finished(Track()) + smc_ftp_track_observer.track_finished(Track()) diff --git a/tests/test_track_observer_tickertrack.py b/tests/test_track_observer_tickertrack.py index 5e11fd47..a9a56f8d 100644 --- a/tests/test_track_observer_tickertrack.py +++ b/tests/test_track_observer_tickertrack.py @@ -1,6 +1,6 @@ """Tests for :class:`observer.TickerTrackObserver`.""" -import os +from pathlib import Path import pytest @@ -10,18 +10,18 @@ @pytest.mark.filterwarnings( - f"ignore:{_FORMAT_WARNING}:PendingDeprecationWarning:nowplaying.track.observer" + f"ignore:{_FORMAT_WARNING}:PendingDeprecationWarning:nowplaying.track.observer", ) def test_init(): """Test class:`TickerTrackObserver`'s :meth:`.__init__` method.""" ticker_track_observer = TickerTrackObserver( - options=TickerTrackObserver.Options(file_path="") + options=TickerTrackObserver.Options(file_path=""), ) assert ticker_track_observer.ticker_file_path == "" @pytest.mark.filterwarnings( - f"ignore:{_FORMAT_WARNING}:PendingDeprecationWarning:nowplaying.track.observer" + f"ignore:{_FORMAT_WARNING}:PendingDeprecationWarning:nowplaying.track.observer", ) def test_track_started(track_factory, show_factory): """Test :class:`TickerTrackObserver`'s :meth:`track_started` method.""" @@ -30,21 +30,21 @@ def test_track_started(track_factory, show_factory): track.show = show_factory() ticker_track_observer = TickerTrackObserver( - options=TickerTrackObserver.Options(file_path="/tmp/track_started.xml") + options=TickerTrackObserver.Options(file_path="/tmp/track_started.xml"), ) ticker_track_observer.track_started(track) - assert os.path.exists("/tmp/track_started.xml") + assert Path("/tmp/track_started.xml").exists() @pytest.mark.filterwarnings( - f"ignore:{_FORMAT_WARNING}:PendingDeprecationWarning:nowplaying.track.observer" + f"ignore:{_FORMAT_WARNING}:PendingDeprecationWarning:nowplaying.track.observer", ) def test_track_finished(track_factory): """Test :class:`TickerTrackObserver`'s :meth:`track_finished` method.""" track = track_factory() ticker_track_observer = TickerTrackObserver( - options=TickerTrackObserver.Options(file_path="/tmp/dummy.xml") + options=TickerTrackObserver.Options(file_path="/tmp/dummy.xml"), ) - assert ticker_track_observer.track_finished(track) + ticker_track_observer.track_finished(track)