From c43e6ee045acec2018fe337c56ca6e854c324ef6 Mon Sep 17 00:00:00 2001 From: Jan Willhaus Date: Sun, 7 Jan 2024 18:36:18 +0100 Subject: [PATCH] feat: Integrate click for cli and config-parsing --- podcast_archiver/__main__.py | 40 ------ podcast_archiver/argparse.py | 84 ------------ podcast_archiver/base.py | 8 +- podcast_archiver/cli.py | 200 +++++++++++++++++++++++++++ podcast_archiver/config.py | 236 +++++++++++++++++--------------- podcast_archiver/constants.py | 4 +- poetry.lock | 247 +++++++++++++++++++++++----------- pyproject.toml | 12 +- tests/test_config.py | 22 ++- tests/test_main.py | 46 +++---- 10 files changed, 542 insertions(+), 357 deletions(-) delete mode 100644 podcast_archiver/__main__.py delete mode 100644 podcast_archiver/argparse.py create mode 100644 podcast_archiver/cli.py diff --git a/podcast_archiver/__main__.py b/podcast_archiver/__main__.py deleted file mode 100644 index 6574cfd..0000000 --- a/podcast_archiver/__main__.py +++ /dev/null @@ -1,40 +0,0 @@ -import argparse -import sys -from typing import Union - -from pydantic import ValidationError - -from podcast_archiver import __version__ -from podcast_archiver.argparse import get_parser -from podcast_archiver.base import PodcastArchiver -from podcast_archiver.config import Settings - - -def main(argv: Union[list[str], None] = None) -> None: - try: - parser = get_parser(Settings) - args = parser.parse_args(argv) - if args.version: - print(__version__) - sys.exit(0) - if args.config_generate: - Settings.generate_example(args.config_generate) - sys.exit(0) - - settings = Settings.load_from_yaml(args.config) - settings.merge_argparser_args(args) - if not (settings.opml_files or settings.feeds): - parser.error("Must provide at least one of --feed or --opml") - - pa = PodcastArchiver(settings) - pa.run() - except KeyboardInterrupt: - sys.exit("\nERROR: Interrupted by user") - except (FileNotFoundError, ValidationError) as error: - sys.exit("\nERROR: %s" % error) - except argparse.ArgumentTypeError as error: - parser.error(str(error)) - - -if __name__ == "__main__": # pragma: no cover - main() diff --git a/podcast_archiver/argparse.py b/podcast_archiver/argparse.py deleted file mode 100644 index 6b07f95..0000000 --- a/podcast_archiver/argparse.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import TYPE_CHECKING - -from pydantic import types - -if TYPE_CHECKING: - from pydantic import BaseSettings - from pydantic.fields import ModelField - - -class readable_file(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - prospective_file = Path(values) - if not prospective_file.is_file(): - raise argparse.ArgumentTypeError(f"{prospective_file} does not exist") - setattr(namespace, self.dest, prospective_file) - - -class writeable_config_file(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - prospective_file = Path(values) - if prospective_file.suffix not in {".yml", ".yaml"}: - raise argparse.ArgumentTypeError(f"{prospective_file} must end in '.yml' or '.yaml'") - if not prospective_file.parent.is_dir(): - raise argparse.ArgumentTypeError(f"{prospective_file.parent} does not exist") - if prospective_file.is_file(): - raise argparse.ArgumentTypeError(f"{prospective_file} must not exist yet") - setattr(namespace, self.dest, prospective_file) - - -def _convert_field_type(field: ModelField): - if field.type_ in {types.DirectoryPath, types.FilePath}: - return str - return field.type_ - - -def get_parser(settings_class: type[BaseSettings]): - parser = argparse.ArgumentParser(prog="podcast-archiver") - for name, field in settings_class.__fields__.items(): - if not (flags := field.field_info.extra.get("flags")): - continue - - added_args = {} - if action := field.field_info.extra.get("argparse_action"): - added_args["action"] = action - elif field.default is False: - added_args["action"] = "store_true" - elif field.default is True: - added_args["action"] = "store_false" - else: - added_args["type"] = _convert_field_type(field) - - if metavar := field.field_info.extra.get("argparse_metavar"): - added_args["metavar"] = metavar - - parser.add_argument( - *flags, - dest=name, - default=field.default, - help=field.field_info.description, - **added_args, - ) - - parser.add_argument( - "--config", - action=readable_file, - help=( - "Provide a path to a config file. Additional command line" - " arguments will override the settings found in this file." - ), - metavar="CONFIG_FILE", - ) - parser.add_argument( - "--config-generate", - action=writeable_config_file, - help="Generate an example YAML config file at the given location and exit. Must end in '.yml' or '.yaml'.", - metavar="CONFIG_FILE", - ) - parser.add_argument("-V", "--version", action="store_true", help="Print version and exit.") - - return parser diff --git a/podcast_archiver/base.py b/podcast_archiver/base.py index 6954278..22614bf 100755 --- a/podcast_archiver/base.py +++ b/podcast_archiver/base.py @@ -18,7 +18,7 @@ from podcast_archiver import constants if TYPE_CHECKING: - from podcast_archiver.config import Settings + from podcast_archiver.settings import Settings class PodcastArchiver: @@ -196,7 +196,7 @@ def truncateLinkList(self, linklist, feed_info): link = episode_dict["url"] filename = self.linkToTargetFilename(link, feed_info) - if path.isfile(filename): + if filename and path.isfile(filename): del linklist[index:] if self.verbose > 1: print(f" found existing episodes, {len(linklist)} new to process") @@ -221,7 +221,7 @@ def parseFeedInfo(self, feedobj): return None def processPodcastLink(self, feed_next_page): - feed_info = None + feed_info = {} linklist = [] while True: if not (feedobj := self.getFeedObj(feed_next_page)): @@ -255,7 +255,7 @@ def checkEpisodeExistsPreflight(self, link, *, feed_info, episode_dict): if self.verbose > 1: print("\tLocal filename:", filename) - if path.isfile(filename): + if filename and path.isfile(filename): if self.verbose > 1: print("\t✓ Already exists.") return None diff --git a/podcast_archiver/cli.py b/podcast_archiver/cli.py new file mode 100644 index 0000000..6345141 --- /dev/null +++ b/podcast_archiver/cli.py @@ -0,0 +1,200 @@ +import pathlib +from typing import Any + +import rich_click as click +from pydantic import ValidationError + +from podcast_archiver import __version__ as version +from podcast_archiver.base import PodcastArchiver +from podcast_archiver.config import DEFAULT_SETTINGS, Settings +from podcast_archiver.constants import ENVVAR_PREFIX, PROG_NAME + +click.rich_click.USE_RICH_MARKUP = True +click.rich_click.USE_MARKDOWN = True +click.rich_click.OPTIONS_PANEL_TITLE = "Miscellaneous Options" +click.rich_click.OPTION_GROUPS = { + PROG_NAME: [ + { + "name": "Basic parameters", + "options": [ + "--feed", + "--opml", + "--dir", + "--config", + ], + }, + { + "name": "Processing parameters", + "options": [ + "--subdirs", + "--update", + "--slugify", + "--max-episodes", + "--date-prefix", + ], + }, + ] +} + + +@click.command( + context_settings={ + "auto_envvar_prefix": ENVVAR_PREFIX, + }, + help="Archive all of your favorite podcasts", +) +@click.help_option("-h", "--help") +@click.option( + "-f", + "--feed", + multiple=True, + show_envvar=True, + help="Feed URLs to archive. Use repeatedly for multiple feeds.", +) +@click.option( + "-o", + "--opml", + multiple=True, + show_envvar=True, + help=( + "OPML files (as exported by many other podcatchers) containing feed URLs to archive. " + "Use repeatedly for multiple files." + ), +) +@click.option( + "-d", + "--dir", + type=click.Path( + exists=False, + writable=True, + file_okay=False, + dir_okay=True, + resolve_path=True, + path_type=pathlib.Path, + ), + show_default=True, + required=False, + default=DEFAULT_SETTINGS.archive_directory, + show_envvar=True, + help="Directory to which to download the podcast archive", +) +@click.option( + "-s", + "--subdirs", + type=bool, + default=DEFAULT_SETTINGS.create_subdirectories, + is_flag=True, + show_envvar=True, + help="Place downloaded podcasts in separate subdirectories per podcast (named with their title).", +) +@click.option( + "-u", + "--update", + type=bool, + default=DEFAULT_SETTINGS.update_archive, + is_flag=True, + show_envvar=True, + help=( + "Update the feeds with newly added episodes only. " + "Adding episodes ends with the first episode already present in the download directory." + ), +) +@click.option( + "-p", + "--progress", + type=bool, + default=DEFAULT_SETTINGS.show_progress_bars, + is_flag=True, + show_envvar=True, + help="Show progress bars while downloading episodes.", +) +@click.option( + "-v", + "--verbose", + count=True, + default=DEFAULT_SETTINGS.verbose, + help="Increase the level of verbosity while downloading.", +) +@click.option( + "-S", + "--slugify", + type=bool, + default=DEFAULT_SETTINGS.slugify_paths, + is_flag=True, + show_envvar=True, + help="Format filenames in the most compatible way, replacing all special characters.", +) +@click.option( + "-m", + "--max-episodes", + type=int, + default=DEFAULT_SETTINGS.maximum_episode_count, + help=( + "Only download the given number of episodes per podcast feed. " + "Useful if you don't really need the entire backlog." + ), +) +@click.option( + "--date-prefix", + type=bool, + default=DEFAULT_SETTINGS.add_date_prefix, + is_flag=True, + show_envvar=True, + help="Prefix episodes with their publishing date. Useful to ensure chronological ordering.", +) +@click.version_option( + version, + "-V", + "--version", + prog_name=PROG_NAME, +) +@click.option( + "--config-generate", + type=bool, + expose_value=False, + is_flag=True, + callback=Settings.click_callback_generate, + is_eager=True, + help="Emit an example YAML config file to stdout and exit.", +) +@click.option( + "-c", + "--config", + type=click.Path( + readable=True, + file_okay=True, + dir_okay=False, + resolve_path=True, + path_type=pathlib.Path, + ), + expose_value=False, + default=pathlib.Path(click.get_app_dir(PROG_NAME)) / "config.yaml", + show_default=True, + callback=Settings.click_callback_load, + is_eager=True, + show_envvar=True, + help="Path to a config file. Command line arguments will take precedence.", +) +@click.pass_context +def main(ctx: click.RichContext, **kwargs: Any) -> int: + try: + config = Settings.model_validate(kwargs) + + # Replicate click's `no_args_is_help` behavior but only when config file does not contain feeds/OPMLs + if not (config.feeds or config.opml_files): + click.echo(ctx.command.get_help(ctx)) + return 0 + + pa = PodcastArchiver(config) + pa.run() + except KeyboardInterrupt as exc: + raise click.Abort("Interrupted by user") from exc + except FileNotFoundError as exc: + raise click.Abort(exc) from exc + except ValidationError as exc: + raise click.Abort(f"Invalid settings: {exc}") from exc + return 0 + + +if __name__ == "__main__": + main.main(prog_name=PROG_NAME) diff --git a/podcast_archiver/config.py b/podcast_archiver/config.py index 572074b..320ffca 100644 --- a/podcast_archiver/config.py +++ b/podcast_archiver/config.py @@ -1,160 +1,184 @@ from __future__ import annotations +import pathlib import textwrap -from os import environ -from pathlib import Path -from typing import TYPE_CHECKING, Any, Union - -from pydantic import BaseSettings, DirectoryPath, Field, FilePath, validator +from datetime import datetime +from typing import IO, TYPE_CHECKING + +import pydantic +import rich_click as click +from pydantic import BaseModel, BeforeValidator, DirectoryPath, Field, FilePath +from pydantic import ConfigDict as _ConfigDict +from pydantic_core import to_json +from typing_extensions import Annotated from yaml import safe_load -from podcast_archiver import __version__ +from podcast_archiver import __version__ as version if TYPE_CHECKING: - import argparse + pass + + +def defaultcwd(v: pathlib.Path | None) -> pathlib.Path: + if not v: + return pathlib.Path.cwd() + return v + + +def expanduser(v: pathlib.Path) -> pathlib.Path: + if isinstance(v, str): + v = pathlib.Path(v) + return v.expanduser() + + +UserExpandedDir = Annotated[DirectoryPath, BeforeValidator(expanduser), BeforeValidator(defaultcwd)] +UserExpandedFile = Annotated[FilePath, BeforeValidator(expanduser)] -class Settings(BaseSettings): - class Config: - env_prefix = "PODCAST_ARCHIVER_" +class Settings(BaseModel): + model_config = _ConfigDict(populate_by_name=True) feeds: list[str] = Field( default_factory=list, - flags=("-f", "--feed"), - description=( - "Provide feed URLs to the archiver. The command line flag can be used repeatedly to input multiple feeds." - ), - argparse_action="append", - argparse_metavar="FEED_URL_OR_FILE", + alias="feed", + description="List of feed URLs to archive.", ) - opml_files: list[FilePath] = Field( + + opml_files: list[UserExpandedFile] = Field( default_factory=list, - flags=("-o", "--opml"), + alias="opml", description=( - "Provide an OPML file (as exported by many other podcatchers) containing your feeds. The parameter can be" - " used multiple times, once for every OPML file." + "OPML files containing feed URLs to archive. OPML files can be exported from a variety of podcatchers." ), - argparse_action="append", - argparse_metavar="OPML_FILE", ) - archive_directory: DirectoryPath = Field( # noqa: A003 - None, - flags=("-d", "--dir"), - description="Set the output directory of the podcast archive.", + + archive_directory: UserExpandedDir = Field( + default=None, alias="dir", description="Directory to which to download the podcast archive." ) create_subdirectories: bool = Field( - False, - flags=("-s", "--subdirs"), - description="Place downloaded podcasts in separate subdirectories per podcast (named with their title).", + default=False, + alias="subdirs", + description="Creates one directory per podcast (named with their title) within the archive directory.", ) + update_archive: bool = Field( - False, - flags=("-u", "--update"), + default=False, + alias="update", description=( - "Force the archiver to only update the feeds with newly added episodes. As soon as the first old episode" - " found in the download directory, further downloading is interrupted." + "Update the feeds with newly added episodes only. " + "Adding episodes ends with the first episode already present in the download directory." ), ) + verbose: int = Field( - 0, - flags=("-v", "--verbose"), + default=0, + alias="verbose", description="Increase the level of verbosity while downloading.", - argparse_action="count", ) + show_progress_bars: bool = Field( - False, - flags=("-p", "--progress"), + default=False, + alias="progress", description="Show progress bars while downloading episodes.", ) slugify_paths: bool = Field( - False, - flags=("-S", "--slugify"), - description=( - "Clean all folders and filename of potentially weird characters that might cause trouble with one or" - " another target filesystem." - ), + default=False, + alias="slugify", + description="Format filenames in the most compatible way, replacing all special characters.", ) maximum_episode_count: int = Field( - 0, - flags=("-m", "--max-episodes"), + default=0, + alias="max_episodes", description=( - "Only download the given number of episodes per podcast feed. Useful if you don't really need the entire" - " backlog." + "Only download the given number of episodes per podcast feed. " + "Useful if you don't really need the entire backlog." ), ) add_date_prefix: bool = Field( - False, - flags=("--date-prefix",), - description=( - "Prefix all episodes with an ISO8602 formatted date of when they were published. Useful to ensure" - " chronological ordering." - ), + default=False, + alias="date_prefix", + description="Prefix episodes with their publishing date. Useful to ensure chronological ordering.", ) - @validator("archive_directory", pre=True) - def normalize_archive_directory(cls, v) -> Path: - if v is None: - return Path.cwd() - return Path(v).expanduser() - - @validator("opml_files", pre=True, each_item=True) - def normalize_opml_files(cls, v: Any) -> Path: - return Path(v).expanduser() + # @field_validator("archive_directory") + # def normalize_archive_directory(cls, v) -> pathlib.Path: + # if v is None: + # return pathlib.Path.cwd() + # return pathlib.Path(v).expanduser() @classmethod - def load_from_yaml(cls, path: Union[Path, None]) -> Settings: - target = None - if path and path.is_file(): - target = path - else: - target = cls._get_envvar_config_path() - - if target: - with target.open("r") as filep: - content = safe_load(filep) - if content: - return cls.parse_obj(content) + def load_from_yaml(cls, path: pathlib.Path) -> Settings: + with path.open("r") as filep: + content = safe_load(filep) + if content: + try: + return cls.model_validate(content) + except pydantic.ValidationError as exc: + click.echo(f"Config file {path} is invalid:", err=True) + + for error in exc.errors(): + click.echo(f"* {error['msg']}: {error['loc'][0]}", err=True) + raise click.Abort from exc return cls() # type: ignore[call-arg] @classmethod - def _get_envvar_config_path(cls) -> Union[Path, None]: - if not (var_value := environ.get(f"{cls.Config.env_prefix}CONFIG")): + def click_callback_load( + cls, ctx: click.Context, param: click.Parameter, value: pathlib.Path | None + ) -> pathlib.Path | None: + if not value or ctx.resilient_parsing: return None - if not (env_path := Path(var_value).expanduser()).is_file(): - raise FileNotFoundError(f"{env_path} does not exist") - - return env_path - - def merge_argparser_args(self, args: argparse.Namespace): - for name, field in self.__fields__.items(): - if (args_value := getattr(args, name, None)) is None: - continue - - settings_value = getattr(self, name) - if isinstance(settings_value, list) and isinstance(args_value, list): - setattr(self, name, settings_value + args_value) - continue - - if args_value != field.get_default(): - setattr(self, name, args_value) - - merged_settings = self.dict(exclude_defaults=True, exclude_unset=True) - return self.parse_obj(merged_settings) + if not value.exists(): + if value == param.default: + with value.open("w") as fp: + cls.click_callback_generate(ctx, param, True, file=fp) + else: + click.echo(f"Config file {value} does not exist", err=True) + raise click.Abort + ctx.default_map = ctx.default_map or {} + settings = cls.load_from_yaml(value) + ctx.default_map.update(settings.model_dump(exclude_unset=True, exclude_none=True, by_alias=True)) + return value @classmethod - def generate_example(cls, path: Path) -> None: - text = ["## Configuration for podcast-archiver"] - text.append(f"## Generated with version {__version__}\n") - for name, field in cls.__fields__.items(): - text.extend( - textwrap.wrap(f"## Field '{name}': {field.field_info.description}\n", subsequent_indent="## ") + def click_callback_generate( + cls, ctx: click.Context, param: click.Parameter, value: bool, file: IO | None = None + ) -> None: + if not value or ctx.resilient_parsing: + return + + now = datetime.now().replace(microsecond=0).astimezone() + click.echo( + (f"## Configuration for podcast-archiver\n## Generated with version {version} at {now}\n"), file=file + ) + wrapper = textwrap.TextWrapper(width=80, initial_indent="## ", subsequent_indent="## ") + + cli_params = {p.name: p for p in ctx.command.get_params(ctx)} + for name, field in cls.model_fields.items(): + cli_param = cli_params[field.alias] + if (cli_default := cli_param.get_default(ctx, call=True)) is not None: + value = cli_default + else: + value = field.get_default(call_default_factory=True) + click.echo( + "\n".join( + [ + *wrapper.wrap(f"Field '{name}': {field.description}"), + "##", + *wrapper.wrap(f"Equivalent command line option: {', '.join(cli_param.opts)}"), + "##", + f"{name}: {to_json(value).decode()}\n", # Pydantic's dumper supports supports Path, etc. + ] + ), + file=file, ) - text.append(f"# {name}: {field.get_default()}\n") + if file: + return + ctx.exit() + - with path.open("w") as filep: - filep.write("\n".join(text)) +DEFAULT_SETTINGS = Settings() diff --git a/podcast_archiver/constants.py b/podcast_archiver/constants.py index 40c22bb..fb304a7 100644 --- a/podcast_archiver/constants.py +++ b/podcast_archiver/constants.py @@ -1,3 +1,5 @@ from podcast_archiver import __version__ -USER_AGENT = f"podcast-archiver/{__version__} (https://github.com/janw/podcast-archiver)" +PROG_NAME = "podcast-archiver" +USER_AGENT = f"{PROG_NAME}/{__version__} (https://github.com/janw/podcast-archiver)" +ENVVAR_PREFIX = "PODCAST_ARCHIVER" diff --git a/poetry.lock b/poetry.lock index 109ba98..53a64f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,27 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + [[package]] name = "argcomplete" version = "3.1.6" @@ -433,16 +455,17 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < [[package]] name = "ipython" -version = "8.18.0" +version = "8.17.2" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.9" files = [ - {file = "ipython-8.18.0-py3-none-any.whl", hash = "sha256:d538a7a98ad9b7e018926447a5f35856113a85d08fd68a165d7871ab5175f6e0"}, - {file = "ipython-8.18.0.tar.gz", hash = "sha256:4feb61210160f75e229ce932dbf8b719bff37af123c0b985fd038b14233daa16"}, + {file = "ipython-8.17.2-py3-none-any.whl", hash = "sha256:1e4d1d666a023e3c93585ba0d8e962867f7a111af322efff6b9c58062b3e5444"}, + {file = "ipython-8.17.2.tar.gz", hash = "sha256:126bb57e1895594bb0d91ea3090bbd39384f6fe87c3d57fd558d0670f50339bb"}, ] [package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} @@ -554,16 +577,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -822,55 +835,139 @@ tests = ["pytest"] [[package]] name = "pydantic" -version = "1.10.13" -description = "Data validation and settings management using python type hints" +version = "2.5.3" +description = "Data validation using Python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, - {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, - {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, - {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, - {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, - {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" @@ -988,7 +1085,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -996,15 +1092,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {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_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"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1021,7 +1110,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1029,7 +1117,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1281,13 +1368,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "types-python-dateutil" -version = "2.8.19.14" +version = "2.8.19.20240106" description = "Typing stubs for python-dateutil" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, + {file = "types-python-dateutil-2.8.19.20240106.tar.gz", hash = "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f"}, + {file = "types_python_dateutil-2.8.19.20240106-py3-none-any.whl", hash = "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"}, ] [[package]] @@ -1303,13 +1390,13 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.20231231" +version = "2.31.0.20240106" description = "Typing stubs for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20231231.tar.gz", hash = "sha256:0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee"}, - {file = "types_requests-2.31.0.20231231-py3-none-any.whl", hash = "sha256:2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027"}, + {file = "types-requests-2.31.0.20240106.tar.gz", hash = "sha256:0e1c731c17f33618ec58e022b614a1a2ecc25f7dc86800b36ef341380402c612"}, + {file = "types_requests-2.31.0.20240106-py3-none-any.whl", hash = "sha256:da997b3b6a72cc08d09f4dba9802fdbabc89104b35fe24ee588e674037689354"}, ] [package.dependencies] @@ -1317,13 +1404,13 @@ urllib3 = ">=2" [[package]] name = "types-tqdm" -version = "4.66.0.5" +version = "4.66.0.20240106" description = "Typing stubs for tqdm" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-tqdm-4.66.0.5.tar.gz", hash = "sha256:74bd7e469238c28816300f72a9b713d02036f6b557734616430adb7b7e74112c"}, - {file = "types_tqdm-4.66.0.5-py3-none-any.whl", hash = "sha256:d2c38085bec440e8ad1e94e8619f7cb3d1dd0a7ee06a863ccd0610a5945046ef"}, + {file = "types-tqdm-4.66.0.20240106.tar.gz", hash = "sha256:7acf4aade5bad3ded76eb829783f9961b1c2187948eaa6dd1ae8644dff95a938"}, + {file = "types_tqdm-4.66.0.20240106-py3-none-any.whl", hash = "sha256:7459b0f441b969735685645a5d8480f7912b10d05ab45f99a2db8a8e45cb550b"}, ] [[package]] @@ -1375,13 +1462,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "wcwidth" -version = "0.2.12" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, - {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] @@ -1402,4 +1489,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5d1f4cebd4a7ac955f748f8bb7d38f80690be6b09de7f32d1df2811ecb6bac06" +content-hash = "ace4072f2f575b27a5507963ca9c3807ebebfc7962a0fae3a0b4fc50a8a1a3d0" diff --git a/pyproject.toml b/pyproject.toml index 1658c94..124c06f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ repository = "https://codeberg.org/janw/podcast-archiver" "Bug Reports" = "https://codeberg.org/janw/podcast-archiver/issues" [tool.poetry.scripts] -podcast-archiver = 'podcast_archiver.__main__:main' +podcast-archiver = 'podcast_archiver.cli:main' [tool.poetry.dependencies] python = "^3.10" @@ -32,13 +32,13 @@ python-dateutil = "^2.8.2" feedparser = "^6.0.10" tqdm = "^4.65.0" requests = "^2.29.0" -pydantic = "^1.10.7" +pydantic = "^2.5.3" platformdirs = ">=3.4,<5.0" pyyaml = "^6.0" rich-click = "^1.7.2" [tool.poetry.group.dev.dependencies] -ipython = "*" +ipython = "<8.18" ipdb = "*" ruff = "^0.1.11" pre-commit = "^3.2.2" @@ -96,9 +96,9 @@ max-complexity = 8 minversion = "6.0" testpaths = ["tests",] addopts = "--cov podcast_archiver --cov-report term --no-cov-on-fail" -env = [ - "PODCAST_ARCHIVER_CONFIG=", -] + +[tool.pytest_env] +PODCAST_ARCHIVER_CONFIG = "" [tool.coverage.run] omit = [ diff --git a/tests/test_config.py b/tests/test_config.py index 8409266..4024617 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,27 +7,25 @@ DUMMY_FEED = "http://localhost/feed.rss" -def test_load_from_envvar_config_path(tmp_path_cd: Path, monkeypatch): +def test_load(tmp_path_cd: Path): configfile = tmp_path_cd / "configtmp.yaml" configfile.write_text(f"feeds: [{DUMMY_FEED}]") - monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", configfile) - settings = Settings.load_from_yaml(None) + settings = Settings.load_from_yaml(configfile) assert DUMMY_FEED in settings.feeds -def test_load_from_envvar_config_path_nonexistent(monkeypatch): - monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", "nonexistent") +def test_load_nonexistent(): with pytest.raises(FileNotFoundError): - Settings.load_from_yaml(None) + Settings.load_from_yaml(Path("/nonexistent")) -def test_load_from_envvar_config_path_overridden_by_arg(tmp_path_cd: Path, monkeypatch): - configfile = tmp_path_cd / "configtmp.yaml" - configfile.write_text(f"feeds: [{DUMMY_FEED}]") +# def test_load_from_envvar_config_path_overridden_by_arg(tmp_path_cd: Path, monkeypatch): +# configfile = tmp_path_cd / "configtmp.yaml" +# configfile.write_text(f"feeds: [{DUMMY_FEED}]") - monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", "nonexistent") - settings = Settings.load_from_yaml(configfile) +# monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", "nonexistent") +# settings = Settings.load_from_yaml(configfile) - assert DUMMY_FEED in settings.feeds +# assert DUMMY_FEED in settings.feeds diff --git a/tests/test_main.py b/tests/test_main.py index 4d1bbaa..7c750fa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,14 +1,14 @@ from pathlib import Path from unittest.mock import patch +import click import pytest -import podcast_archiver -from podcast_archiver.__main__ import main +from podcast_archiver import cli def test_main(tmp_path_cd: Path, feed_lautsprecher): - main(["--feed", feed_lautsprecher]) + cli.main(["--feed", feed_lautsprecher], standalone_mode=False) files = list(tmp_path_cd.glob("*.m4a")) assert len(files) == 5 @@ -16,7 +16,7 @@ def test_main(tmp_path_cd: Path, feed_lautsprecher): def test_main_interrupted(tmp_path_cd: Path, feed_lautsprecher_notconsumed): with patch("requests.sessions.Session.request", side_effect=KeyboardInterrupt), pytest.raises(SystemExit): - main(["--feed", feed_lautsprecher_notconsumed]) + cli.main(["--feed", feed_lautsprecher_notconsumed]) files = list(tmp_path_cd.glob("*.m4a")) assert len(files) == 0 @@ -24,25 +24,24 @@ def test_main_interrupted(tmp_path_cd: Path, feed_lautsprecher_notconsumed): def test_main_nonexistent_dir(feed_lautsprecher_notconsumed): with pytest.raises(SystemExit): - main(("--feed", feed_lautsprecher_notconsumed, "-d", "/nonexistent")) + cli.main(("--feed", feed_lautsprecher_notconsumed, "-d", "/nonexistent")) @pytest.mark.parametrize("flag", ["-V", "--version"]) -@patch.object(podcast_archiver.__main__, "__version__", "123.4.5") def test_main_print_version(flag, tmp_path_cd: Path, capsys): with pytest.raises(SystemExit): - main([flag]) + cli.main([flag]) captured = capsys.readouterr() - assert captured.out == "123.4.5\n" + assert captured.out.strip().endswith(cli.version) files = list(tmp_path_cd.glob("*.m4a")) assert len(files) == 0 def test_main_unknown_arg(tmp_path_cd: Path, feed_lautsprecher_notconsumed): with pytest.raises(SystemExit): - main(["--feedlings", feed_lautsprecher_notconsumed]) + cli.main(["--feedlings", feed_lautsprecher_notconsumed]) files = list(tmp_path_cd.glob("*.m4a")) assert len(files) == 0 @@ -50,7 +49,7 @@ def test_main_unknown_arg(tmp_path_cd: Path, feed_lautsprecher_notconsumed): def test_main_no_args(tmp_path_cd: Path): with pytest.raises(SystemExit): - main([]) + cli.main([]) files = list(tmp_path_cd.glob("*.m4a")) assert len(files) == 0 @@ -60,7 +59,7 @@ def test_main_config_file(tmp_path_cd: Path, feed_lautsprecher): configfile = tmp_path_cd / "configtmp.yaml" configfile.write_text(f"feeds: [{feed_lautsprecher}]") - main(["--config", str(configfile)]) + cli.main(["--config", str(configfile)], standalone_mode=False) files = list(tmp_path_cd.glob("*.m4a")) assert len(files) == 5 @@ -69,22 +68,21 @@ def test_main_config_file(tmp_path_cd: Path, feed_lautsprecher): def test_main_config_file_notfound(feed_lautsprecher_notconsumed, capsys): configfile = "/nonexistent/configtmp.yaml" - with pytest.raises(SystemExit): - main(["--feed", feed_lautsprecher_notconsumed, "--config", str(configfile)]) + with pytest.raises(click.Abort): + cli.main(["--feed", feed_lautsprecher_notconsumed, "--config", str(configfile)], standalone_mode=False) captured = capsys.readouterr() - assert "error: /nonexistent/configtmp.yaml does not exist" in captured.err + assert "Config file /nonexistent/configtmp.yaml does not exist" in captured.err -def test_main_config_file_generate(tmp_path_cd: Path): +def test_main_config_file_generate(tmp_path_cd: Path, capsys): configfile = tmp_path_cd / "configtmp.yaml" - with pytest.raises(SystemExit): - main(["--config-generate", str(configfile)]) - - content = configfile.read_text() - assert content - assert "## Field 'feeds': " in content - assert "# feeds: []\n" in content - assert "## Field 'archive_directory': " in content - assert "# archive_directory: None\n" in content + cli.main(["--config-generate", str(configfile)], standalone_mode=False) + + captured = capsys.readouterr() + assert captured.out + assert "## Field 'feeds': " in captured.out + assert "feeds: []\n" in captured.out + assert "## Field 'archive_directory': " in captured.out + assert "archive_directory: null\n" in captured.out