From c7b348f4df1454992213f15e556298d3c0b0c2a0 Mon Sep 17 00:00:00 2001 From: GitBib <15717621+GitBib@users.noreply.github.com> Date: Wed, 12 Nov 2025 02:54:32 +0300 Subject: [PATCH 1/5] Add style filtering for ASS subtitles with tests and CLI improvements - Introduced `include_styles` and `exclude_styles` options in the `Subtitle` class for advanced filtering. - Added support for `only_default_style` flag to target "Default" styles specifically. - Expanded tests to verify style filtering logic and edge cases. - Implemented CLI commands for listing unique styles and improved export behavior with style filters. - Updated README with detailed examples for style filtering and CLI enhancements. --- README.md | 118 +++++++++++- pyasstosrt/batch.py | 328 +++++++++++++++++++++++++++++----- pyasstosrt/pyasstosrt.py | 65 ++++++- tests/conftest.py | 5 + tests/sub_with_styles.ass | 47 +++++ tests/test_batch.py | 16 +- tests/test_style_filtering.py | 173 ++++++++++++++++++ 7 files changed, 690 insertions(+), 62 deletions(-) create mode 100644 tests/sub_with_styles.ass create mode 100644 tests/test_style_filtering.py diff --git a/README.md b/README.md index 9452eb3..4c31793 100644 --- a/README.md +++ b/README.md @@ -54,31 +54,131 @@ from pyasstosrt import Subtitle sub = Subtitle('sub.ass', remove_duplicates=True) sub.export() ``` + +You can get a list of all styles in the file and filter subtitles by style names. + +```python +from pyasstosrt import Subtitle + +# Get list of all styles in the file +sub = Subtitle('sub.ass') +styles = sub.get_styles() +print(styles) # ['Default', 'Alt', 'Signs', 'Credits'] + +# Export only styles with "Default" in name (e.g., Default, Default_dvd) +sub = Subtitle('sub.ass', only_default_style=True) +sub.export() + +# Export only Default style +sub = Subtitle('sub.ass', include_styles=['Default']) +sub.export() + +# Export only Default and Alt styles +sub = Subtitle('sub.ass', include_styles=['Default', 'Alt']) +sub.export() + +# Exclude Signs and Credits styles +sub = Subtitle('sub.ass', exclude_styles=['Signs', 'Credits']) +sub.export() +``` + CLI ------------ + +### 🎬 Export Command + +Convert ASS/SSA subtitle files to SRT format with various options. + +**Basic usage:** +```bash +pyasstosrt export subtitle.ass +``` + +**Specify output directory:** +```bash +pyasstosrt export subtitle.ass --output-dir /path/to/output +# or use short form +pyasstosrt export subtitle.ass -o /path/to/output +``` + +**Remove effects and duplicates:** +```bash +pyasstosrt export subtitle.ass --remove-effects --remove-duplicates +# or use short form +pyasstosrt export subtitle.ass -r -d +``` + +**Process multiple files at once:** +```bash +pyasstosrt export subtitle1.ass subtitle2.ass subtitle3.ass +``` + +**Style filtering options:** +```bash +# Export only styles with "Default" in name (e.g., Default, Default_dvd) +pyasstosrt export subtitle.ass --only-default + +# Export only specific styles +pyasstosrt export subtitle.ass --include-styles "Default,Signs" + +# Exclude specific styles +pyasstosrt export subtitle.ass --exclude-styles "Signs,Credits" +``` + +**Custom encoding:** ```bash -pyasstosrt export /Users/user/sub/sub.ass +pyasstosrt export subtitle.ass --encoding utf-16 ``` -**Optional** You can specify an export folder. +**Print dialogues to console:** ```bash -pyasstosrt export /Users/user/sub/sub.ass --output-dir /Users/user/sub/srt +pyasstosrt export subtitle.ass --output-dialogues ``` -**Optional** If you want to remove effects from text, you can use the --remove-effects flag. +### 🎨 Styles Command + +List all unique styles found in an ASS subtitle file. + +**Basic usage:** +```bash +pyasstosrt styles subtitle.ass +``` + +**Display in table format:** +```bash +pyasstosrt styles subtitle.ass --table +# or use short form +pyasstosrt styles subtitle.ass -t +``` + +### 🔧 General Options + +**Show version:** ```bash -pyasstosrt export /Users/user/sub/sub.ass --remove-effects --output-dir /Users/user/sub/srt +pyasstosrt --version +# or +pyasstosrt -v ``` -**Optional** If you need to remove duplicates, you can use the --remove-duplicates flag. +**Show help:** ```bash -pyasstosrt export /Users/user/sub/sub.ass --remove-duplicates +pyasstosrt --help +pyasstosrt export --help +pyasstosrt styles --help ``` -**Optional** You can use the flags together --remove-duplicates --remove-effects +**Enable shell completion:** ```bash -pyasstosrt export /Users/user/sub/sub.ass --remove-duplicates --remove-effects +# For bash +pyasstosrt --install-completion bash + +# For zsh +pyasstosrt --install-completion zsh + +# For fish +pyasstosrt --install-completion fish ``` + Installation ------------ Most users will want to simply install the latest version, hosted on PyPI: diff --git a/pyasstosrt/batch.py b/pyasstosrt/batch.py index 00bf370..673e8fa 100644 --- a/pyasstosrt/batch.py +++ b/pyasstosrt/batch.py @@ -1,11 +1,13 @@ from pathlib import Path -from typing import List, Optional +from typing import Annotated, List, Optional try: import typer from rich.console import Console from rich.panel import Panel - from rich.progress import Progress + from rich.progress import Progress, SpinnerColumn, TextColumn + from rich.table import Table + from rich.traceback import install as install_rich_traceback except ModuleNotFoundError as e: raise ImportError( "pyasstosrt was installed without the cli extra. Please reinstall it with: pip install 'pyasstosrt[cli]'" @@ -13,7 +15,17 @@ from pyasstosrt import Subtitle, __version__ -app = typer.Typer(help="Convert ASS subtitles to SRT format") +# Install rich traceback for better error display +install_rich_traceback(show_locals=True) + +app = typer.Typer( + name="pyasstosrt", + help="🎬 Convert ASS/SSA subtitles to SRT format with style filtering", + add_completion=True, + rich_markup_mode="rich", + pretty_exceptions_enable=True, + pretty_exceptions_show_locals=False, +) console = Console() @@ -25,67 +37,299 @@ def version_callback(value: bool): @app.callback() def callback( - version: Optional[bool] = typer.Option( - None, "--version", "-v", callback=version_callback, help="Show version and exit" - ), + version: Annotated[ + Optional[bool], + typer.Option( + "--version", + "-v", + callback=version_callback, + is_eager=True, + help="Show version information and exit", + ), + ] = None, ): """ - PyAssToSrt - Convert ASS subtitles to SRT format + [bold cyan]PyAssToSrt[/bold cyan] - Convert ASS/SSA subtitles to SRT format + + A powerful tool for converting Advanced SubStation Alpha (ASS/SSA) subtitle files + to SubRip (SRT) format with advanced filtering capabilities. """ pass -@app.command() +@app.command(name="export", help="Convert ASS/SSA subtitle file(s) to SRT format") def export( - filepath: List[Path] = typer.Argument( - ..., - help="Path to the ASS file(s)", - exists=True, - file_okay=True, - dir_okay=False, - readable=True, - ), - removing_effects: bool = typer.Option(False, "--remove-effects", "-r", help="Remove effects from subtitles"), - remove_duplicates: bool = typer.Option(False, "--remove-duplicates", "-d", help="Remove duplicate subtitles"), - output_dir: Optional[Path] = typer.Option( - None, - "--output-dir", - "-o", - help="Output directory for the SRT file(s)", - file_okay=False, - dir_okay=True, - writable=True, - ), - encoding: str = typer.Option("utf8", "--encoding", "-e", help="Encoding for the output file"), - output_dialogues: bool = typer.Option(False, "--output-dialogues", "-p", help="Print dialogues to console"), + filepath: Annotated[ + List[Path], + typer.Argument( + help="Path(s) to the ASS/SSA file(s) to convert", + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + show_default=False, + ), + ], + removing_effects: Annotated[ + bool, + typer.Option( + "--remove-effects", + "-r", + help="Remove ASS drawing/animation effects from subtitle text", + show_default=True, + ), + ] = False, + remove_duplicates: Annotated[ + bool, + typer.Option( + "--remove-duplicates", + "-d", + help="Merge consecutive duplicate subtitle lines", + show_default=True, + ), + ] = False, + only_default_style: Annotated[ + bool, + typer.Option( + "--only-default", + "-D", + help="Export only styles containing 'Default' in name (excludes Signs, Credits, etc.)", + show_default=True, + ), + ] = False, + include_styles: Annotated[ + Optional[str], + typer.Option( + "--include-styles", + "-i", + help="Comma-separated list of style names to include (e.g., 'Default,Signs')", + show_default=False, + ), + ] = None, + exclude_styles: Annotated[ + Optional[str], + typer.Option( + "--exclude-styles", + "-x", + help="Comma-separated list of style names to exclude (e.g., 'Signs,Credits_dvd')", + show_default=False, + ), + ] = None, + output_dir: Annotated[ + Optional[Path], + typer.Option( + "--output-dir", + "-o", + help="Output directory for converted SRT file(s). Defaults to source file directory", + file_okay=False, + dir_okay=True, + writable=True, + show_default=False, + ), + ] = None, + encoding: Annotated[ + str, + typer.Option( + "--encoding", + "-e", + help="Text encoding for output SRT file", + show_default=True, + ), + ] = "utf8", + output_dialogues: Annotated[ + bool, + typer.Option( + "--output-dialogues", + "-p", + help="Print converted dialogues to console instead of saving to file", + show_default=True, + ), + ] = False, ): - """Convert ASS subtitle file(s) to SRT format""" - with Progress() as progress: - task = progress.add_task("[green]Converting...", total=len(filepath)) + """ + Convert ASS/SSA subtitle file(s) to SRT format. + + [bold]Examples:[/bold] + pyasstosrt export subtitle.ass + pyasstosrt export subtitle.ass --remove-effects --remove-duplicates + pyasstosrt export subtitle.ass --only-default -o output/ + pyasstosrt export *.ass --include-styles "Default,Alt" + """ + # Validate mutually exclusive style options + style_options_count = sum([only_default_style, bool(include_styles), bool(exclude_styles)]) + if style_options_count > 1: + console.print( + "[red]Error:[/red] Options [bold]--only-default[/bold], [bold]--include-styles[/bold], " + "and [bold]--exclude-styles[/bold] are mutually exclusive. Please use only one.", + style="bold red", + ) + raise typer.Exit(1) + + # Parse style filters + include_styles_list = [s.strip() for s in include_styles.split(",")] if include_styles else None + exclude_styles_list = [s.strip() for s in exclude_styles.split(",")] if exclude_styles else None + + # Show conversion summary + console.print(f"\n[bold cyan]🎬 Starting conversion of {len(filepath)} file(s)[/bold cyan]") + if removing_effects: + console.print(" • Removing ASS effects: [green]✓[/green]") + if remove_duplicates: + console.print(" • Removing duplicates: [green]✓[/green]") + if only_default_style: + console.print(" • Filter: [yellow]Only 'Default' styles[/yellow]") + elif include_styles: + console.print(f" • Filter: [yellow]Include styles: {include_styles}[/yellow]") + elif exclude_styles: + console.print(f" • Filter: [yellow]Exclude styles: {exclude_styles}[/yellow]") + console.print() + + success_count = 0 + error_count = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + task = progress.add_task("[cyan]Converting files...", total=len(filepath)) for file in filepath: - progress.console.print(f"\n[bold blue]Processing: {file.name}[/bold blue]") + progress.console.print(f"[bold blue]📄 Processing:[/bold blue] {file.name}") try: - sub = Subtitle(file, removing_effects, remove_duplicates) + sub = Subtitle( + file, + removing_effects, + remove_duplicates, + only_default_style, + include_styles_list, + exclude_styles_list, + ) result = sub.export(output_dir, encoding, output_dialogues) - if output_dialogues: - progress.console.print(Panel(f"Dialogues for {file.name}:", expand=False)) - for dialogue in result: + if output_dialogues and result: + progress.console.print( + Panel( + f"[bold]Dialogues for {file.name}:[/bold]\nTotal: {len(result)} dialogue(s)", + expand=False, + border_style="green", + ) + ) + for dialogue in result[:5]: # Show first 5 as preview progress.console.print(str(dialogue)) + if len(result) > 5: + progress.console.print(f"... and {len(result) - 5} more dialogue(s)") output_file = Path(output_dir) / f"{file.stem}.srt" if output_dir else file.with_suffix(".srt") - progress.console.print(f"[green]Success:[/green] Converted {file.name} to {output_file}") - except Exception as e: + if not output_dialogues: + progress.console.print(f"[green]✓ Success:[/green] {file.name} → {output_file.name}") + success_count += 1 + + except FileNotFoundError: + progress.console.print(f"[red]✗ Error:[/red] File not found: {file.name}", style="bold red") + error_count += 1 + except PermissionError: progress.console.print( - f"[red]Error:[/red] Failed to convert {file.name}. {str(e)}", - style="bold red", + f"[red]✗ Error:[/red] Permission denied when processing {file.name}", style="bold red" ) + error_count += 1 + except Exception as e: + progress.console.print(f"[red]✗ Error:[/red] Failed to convert {file.name}: {str(e)}", style="bold red") + error_count += 1 progress.update(task, advance=1) - console.print("\n[bold green]Conversion completed![/bold green]") + # Show summary + console.print() + if error_count == 0: + console.print(f"[bold green]✓ Conversion completed successfully![/bold green] ({success_count} file(s))") + else: + console.print( + f"[bold yellow]⚠ Conversion completed with errors:[/bold yellow] " + f"{success_count} successful, {error_count} failed" + ) + if error_count > 0: + raise typer.Exit(1) + + +@app.command(name="styles", help="List all unique styles found in an ASS subtitle file") +def styles( + filepath: Annotated[ + Path, + typer.Argument( + help="Path to the ASS/SSA file to analyze", + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + show_default=False, + ), + ], + table_format: Annotated[ + bool, + typer.Option( + "--table", + "-t", + help="Display styles in a formatted table", + show_default=True, + ), + ] = False, +): + """ + List all unique styles found in an ASS/SSA subtitle file. + + This command helps you identify available styles before using + --include-styles or --exclude-styles options in the export command. + + [bold]Examples:[/bold] + pyasstosrt styles subtitle.ass + pyasstosrt styles subtitle.ass --table + """ + try: + console.print(f"\n[bold cyan]🔍 Analyzing styles in:[/bold cyan] {filepath.name}\n") + + sub = Subtitle(filepath) + style_list = sub.get_styles() + + if not style_list: + console.print("[yellow]⚠ No styles found or file is in SRT format[/yellow]") + console.print("[dim]Note: SRT files don't have style information[/dim]") + return + + if table_format: + # Display as a rich table + table = Table(title=f"Styles in {filepath.name}", show_header=True, header_style="bold cyan") + table.add_column("#", style="dim", width=6, justify="right") + table.add_column("Style Name", style="green") + + for idx, style in enumerate(style_list, start=1): + table.add_row(str(idx), style) + + console.print(table) + else: + # Display as a simple list + console.print(f"[bold blue]Styles found in {filepath.name}:[/bold blue]\n") + for idx, style in enumerate(style_list, start=1): + # Highlight "Default" styles + if "Default" in style: + console.print(f" {idx}. [bold green]{style}[/bold green] [dim](default)[/dim]") + else: + console.print(f" {idx}. [green]{style}[/green]") + + console.print(f"\n[bold]Total:[/bold] [cyan]{len(style_list)}[/cyan] unique style(s)") + + # Show helpful tips + console.print("\n[dim]💡 Tips:[/dim]") + console.print(" [dim]• Use --include-styles to export specific styles[/dim]") + console.print(" [dim]• Use --exclude-styles to skip unwanted styles[/dim]") + console.print(" [dim]• Use --only-default to export only 'Default' styles[/dim]") + + except FileNotFoundError: + console.print(f"[red]✗ Error:[/red] File not found: {filepath}", style="bold red") + raise typer.Exit(1) from None + except Exception as e: + console.print(f"[red]✗ Error:[/red] {str(e)}", style="bold red") + raise typer.Exit(1) from e if __name__ == "__main__": diff --git a/pyasstosrt/pyasstosrt.py b/pyasstosrt/pyasstosrt.py index bf86f2c..dbe6843 100644 --- a/pyasstosrt/pyasstosrt.py +++ b/pyasstosrt/pyasstosrt.py @@ -19,6 +19,12 @@ class Subtitle: :type removing_effects: bool :param remove_duplicates: Whether to remove and merge consecutive duplicate dialogues :type remove_duplicates: bool + :param only_default_style: If True, exports only styles with "Default" in the name (e.g., Default, Default_dvd) + :type only_default_style: bool + :param include_styles: List of styles to include (if specified, only these styles will be exported) + :type include_styles: Optional[List[str]] + :param exclude_styles: List of styles to exclude (if specified, these styles will be filtered out) + :type exclude_styles: Optional[List[str]] :raises FileNotFoundError: If the specified file does not exist @@ -34,6 +40,12 @@ class Subtitle: :type removing_effects: bool :ivar is_remove_duplicates: Flag indicating whether to remove and merge consecutive duplicate dialogues :type is_remove_duplicates: bool + :ivar only_default_style: Flag indicating whether to export only styles with "Default" in name + :type only_default_style: bool + :ivar include_styles: List of styles to include (if specified, only these styles will be exported) + :type include_styles: Optional[List[str]] + :ivar exclude_styles: List of styles to exclude (if specified, these styles will be filtered out) + :type exclude_styles: Optional[List[str]] :Example: @@ -43,7 +55,9 @@ class Subtitle: >>> sub.export("output/directory", encoding="utf-8") """ - dialog_mask = re.compile(r"Dialogue: \d+?,(\d:\d{2}:\d{2}.\d{2}),(\d:\d{2}:\d{2}.\d{2}),.*?,\d+,\d+,\d+,.*?,(.*)") + dialog_mask = re.compile( + r"Dialogue: \d+?,(\d:\d{2}:\d{2}.\d{2}),(\d:\d{2}:\d{2}.\d{2}),(.*?),.*?,\d+,\d+,\d+,.*?,(.*)" + ) effects = re.compile(r"(\s?[ml].+?(-?\d+(\.\d+)?).+?(-?\d+(\.\d+)?).+)") srt_pattern = re.compile(r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})") @@ -52,6 +66,9 @@ def __init__( filepath: Union[str, os.PathLike], removing_effects: bool = False, remove_duplicates: bool = False, + only_default_style: bool = False, + include_styles: Optional[List[str]] = None, + exclude_styles: Optional[List[str]] = None, ): self.filepath = Path(filepath) if not self.filepath.is_file(): @@ -59,8 +76,12 @@ def __init__( self.file: str = self.filepath.stem self.raw_text: str = self.get_text() self.dialogues: List[Dialogue] = [] + self.styles: List[str] = [] self.removing_effects: bool = removing_effects self.is_remove_duplicates: bool = remove_duplicates + self.only_default_style: bool = only_default_style + self.include_styles: Optional[List[str]] = include_styles + self.exclude_styles: Optional[List[str]] = exclude_styles def get_text(self) -> str: """ @@ -71,6 +92,21 @@ def get_text(self) -> str: """ return self.filepath.read_text(encoding="utf8") + def get_styles(self) -> List[str]: + """ + Return all unique style names from the ASS file. + + Styles are collected during conversion. If convert() hasn't been called yet, + this method will call it automatically. + + :return: List of unique style names found in the file + :rtype: List[str] + """ + if not self.styles: + self.convert() + + return self.styles + def is_srt_format(self) -> bool: """ Determines if the file is in SRT format. @@ -101,9 +137,32 @@ def _convert_ass(self): """ cleaning_old_format = re.compile(r"{.*?}") dialogs = re.findall(self.dialog_mask, re.sub(cleaning_old_format, "", self.raw_text)) + + # Collect unique styles + self.styles = sorted(set(d[2] for d in dialogs)) + + # Filter by styles if specified + if self.only_default_style and not self.include_styles and not self.exclude_styles: + # Keep only styles containing "Default" (e.g., Default, Default_dvd, etc.) + dialogs = list(filter(lambda d: "Default" in d[2], dialogs)) + elif self.include_styles: + # Build inclusion set for efficient lookup + include_set = set(self.include_styles) + dialogs = list(filter(lambda d: d[2] in include_set, dialogs)) + elif self.exclude_styles: + # Build exclusion set for efficient lookup + exclude_set = set(self.exclude_styles) + dialogs = list(filter(lambda d: d[2] not in exclude_set, dialogs)) + if self.removing_effects: - dialogs = filter(lambda x: re.sub(self.effects, "", x[2]), dialogs) - dialogs = sorted(list(filter(lambda x: x[2], dialogs))) + dialogs = filter(lambda x: re.sub(self.effects, "", x[3]), dialogs) + dialogs = list(filter(lambda x: x[3], dialogs)) + + # Convert from (start, end, style, text) to (start, end, text) for subtitle_formatting + dialogs = [(d[0], d[1], d[3]) for d in dialogs] + + # Sort by (start, end, text) for chronological and stable order + dialogs = sorted(dialogs) self.subtitle_formatting(dialogs) diff --git a/tests/conftest.py b/tests/conftest.py index 906e39e..5bf629a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,11 @@ def sub_srt(): return Subtitle("tests/test_sample.srt") +@pytest.fixture +def sub_with_styles(): + return Subtitle("tests/sub_with_styles.ass") + + @pytest.fixture def cli_runner(): return CliRunner() diff --git a/tests/sub_with_styles.ass b/tests/sub_with_styles.ass new file mode 100644 index 0000000..da33263 --- /dev/null +++ b/tests/sub_with_styles.ass @@ -0,0 +1,47 @@ +[Script Info] +; Test subtitle file with multiple styles +Title: Test Subtitle File +ScriptType: v4.00+ +WrapStyle: 0 +ScaledBorderAndShadow: yes +YCbCr Matrix: TV.601 +PlayResX: 1280 +PlayResY: 720 + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Arial,72,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,3,2,18,18,15,1 +Style: Alt,Arial,72,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,3,2,18,18,15,1 +Style: Thoughts,Arial,72,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,-1,0,0,100,100,0,0,1,3,3,2,18,18,15,1 +Style: Top,Arial,72,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,3,8,18,18,15,1 +Style: Signs,Arial,83,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,-1,0,0,100,100,0,0,1,7.5,6,5,0,0,0,1 +Style: Credits,Arial,53,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,2.25,1.65,7,53,53,45,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,This is a regular dialogue line. +Dialogue: 0,0:00:03.50,0:00:05.50,Default,,0,0,0,,Another regular dialogue. +Dialogue: 0,0:00:06.00,0:00:08.00,Thoughts,,0,0,0,,This is an internal thought. +Dialogue: 0,0:00:08.50,0:00:10.50,Default,,0,0,0,,Back to normal speaking. +Dialogue: 0,0:00:11.00,0:00:13.00,Alt,,0,0,0,,Alternative style dialogue. +Dialogue: 0,0:00:13.50,0:00:15.50,Thoughts,,0,0,0,,Another thought line. +Dialogue: 0,0:00:16.00,0:00:18.00,Default,,0,0,0,,Regular dialogue continues. +Dialogue: 0,0:00:18.50,0:00:20.50,Top,,0,0,0,,Top positioned text. +Dialogue: 0,0:00:21.00,0:00:23.00,Signs,,0,0,0,,Sign Text +Dialogue: 0,0:00:23.50,0:00:25.50,Default,,0,0,0,,More regular dialogue. +Dialogue: 0,0:00:26.00,0:00:28.00,Alt,,0,0,0,,Alternative style again. +Dialogue: 0,0:00:28.50,0:00:30.50,Default,,0,0,0,,Regular line here. +Dialogue: 0,0:00:31.00,0:00:33.00,Thoughts,,0,0,0,,Thinking about something. +Dialogue: 0,0:00:33.50,0:00:35.50,Signs,,0,0,0,,Another Sign +Dialogue: 0,0:00:36.00,0:00:38.00,Default,,0,0,0,,Normal dialogue line. +Dialogue: 0,0:00:38.50,0:00:40.50,Default,,0,0,0,,Continuing the conversation. +Dialogue: 0,0:00:41.00,0:00:43.00,Alt,,0,0,0,,Alternative perspective. +Dialogue: 0,0:00:43.50,0:00:45.50,Top,,0,0,0,,Text at the top. +Dialogue: 0,0:00:46.00,0:00:48.00,Default,,0,0,0,,Regular dialogue here. +Dialogue: 0,0:00:48.50,0:00:50.50,Thoughts,,0,0,0,,Internal monologue. +Dialogue: 0,0:00:51.00,0:00:53.00,Default,,0,0,0,,Speaking out loud again. +Dialogue: 0,0:00:53.50,0:00:55.50,Credits,,0,0,0,,Translation: Test Team +Dialogue: 0,0:00:56.00,0:00:58.00,Default,,0,0,0,,Final dialogue line. +Dialogue: 0,0:00:58.50,0:01:00.50,Signs,,0,0,0,,End Sign +Dialogue: 0,0:01:01.00,0:01:03.00,Default,,0,0,0,,Last regular line. + diff --git a/tests/test_batch.py b/tests/test_batch.py index 78f8c25..88b62cf 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -14,7 +14,7 @@ def test_version(cli_runner): def test_export_help(cli_runner): result = cli_runner.invoke(app, ["export", "--help"]) assert result.exit_code == 0 - assert "Convert ASS subtitle file(s) to SRT format" in result.stdout + assert "Convert ASS/SSA subtitle file(s) to SRT format" in result.stdout def test_export_file_not_exists(cli_runner): @@ -35,7 +35,7 @@ def test_export_with_existing_ass_file(cli_runner, test_files, cleanup_srt_files result = cli_runner.invoke(app, ["export", str(test_file)]) assert result.exit_code == 0 - assert "Success: Converted sub.ass to" in result.stdout + assert "✓ Success: sub.ass → sub.srt" in result.stdout assert srt_file.exists() srt_content = srt_file.read_text(encoding="utf-8") @@ -146,8 +146,8 @@ def test_export_multiple_files(cli_runner, test_files, cleanup_srt_files): assert f"Processing: {file1.name}" in result.stdout assert f"Processing: {file2.name}" in result.stdout - assert f"Success: Converted {file1.name}" in result.stdout - assert f"Success: Converted {file2.name}" in result.stdout + assert f"✓ Success: {file1.name} →" in result.stdout + assert f"✓ Success: {file2.name} →" in result.stdout assert srt_file1.exists() assert srt_file2.exists() @@ -158,7 +158,7 @@ def test_simple_text_file_conversion(cli_runner, invalid_ass_file): result = cli_runner.invoke(app, ["export", str(invalid_file)]) assert result.exit_code == 0 - assert f"Success: Converted {invalid_file.name}" in result.stdout + assert f"✓ Success: {invalid_file.name} →" in result.stdout srt_file = invalid_file.with_suffix(".srt") assert srt_file.exists() @@ -177,8 +177,8 @@ def mock_init(*args, **kwargs): result = cli_runner.invoke(app, ["export", str(test_file)]) - assert result.exit_code == 0 - assert "Error:" in result.stdout + assert result.exit_code == 1 + assert "✗ Error:" in result.stdout assert f"Failed to convert {test_file.name}" in result.stdout assert "Test error" in result.stdout @@ -199,7 +199,7 @@ def test_export_srt_file_with_cli(cli_runner, test_dir, output_dir): try: result = cli_runner.invoke(app, ["export", str(test_file), "--output-dir", str(output_dir)]) assert result.exit_code == 0 - assert "Success: Converted test_sample.srt to" in result.stdout + assert "✓ Success: test_sample.srt →" in result.stdout expected_output = output_dir / "test_sample.srt" assert expected_output.exists() diff --git a/tests/test_style_filtering.py b/tests/test_style_filtering.py new file mode 100644 index 0000000..f7a10b4 --- /dev/null +++ b/tests/test_style_filtering.py @@ -0,0 +1,173 @@ +import pytest + +from pyasstosrt import Subtitle + + +def test_include_single_style(sub_with_styles): + dialogues = sub_with_styles.export(output_dialogues=True) + total_dialogues = len(dialogues) + + # Test with include_styles - should only get Default style lines + sub_filtered = Subtitle("tests/sub_with_styles.ass", include_styles=["Default"]) + filtered_dialogues = sub_filtered.export(output_dialogues=True) + + assert isinstance(filtered_dialogues, list) + assert len(filtered_dialogues) < total_dialogues + assert len(filtered_dialogues) == 12 # 12 Default style lines in test file + + +def test_include_multiple_styles(): + sub_filtered = Subtitle("tests/sub_with_styles.ass", include_styles=["Default", "Alt"]) + filtered_dialogues = sub_filtered.export(output_dialogues=True) + + assert isinstance(filtered_dialogues, list) + assert len(filtered_dialogues) == 15 # 12 Default + 3 Alt lines + + +def test_exclude_single_style(sub_with_styles): + dialogues = sub_with_styles.export(output_dialogues=True) + total_dialogues = len(dialogues) + + # Exclude Signs style - should get all except 3 Signs lines + sub_filtered = Subtitle("tests/sub_with_styles.ass", exclude_styles=["Signs"]) + filtered_dialogues = sub_filtered.export(output_dialogues=True) + + assert isinstance(filtered_dialogues, list) + assert len(filtered_dialogues) == total_dialogues - 3 # 25 total - 3 Signs = 22 + + +def test_exclude_multiple_styles(): + # Exclude Signs and Credits - should get 25 - 3 Signs - 1 Credits = 21 + sub_filtered = Subtitle("tests/sub_with_styles.ass", exclude_styles=["Signs", "Credits"]) + filtered_dialogues = sub_filtered.export(output_dialogues=True) + + assert isinstance(filtered_dialogues, list) + assert len(filtered_dialogues) == 21 + + +def test_style_filtering_with_export_file(output_dir): + try: + sub = Subtitle("tests/sub_with_styles.ass", include_styles=["Default"]) + sub.export(output_dir) + output_file = output_dir / "sub_with_styles.srt" + assert output_file.is_file() + finally: + if output_file.exists(): + output_file.unlink() + + +def test_style_filtering_with_removing_effects(): + sub = Subtitle("tests/sub_with_styles.ass", removing_effects=True, include_styles=["Default"]) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + assert len(dialogues) == 12 + + +def test_style_filtering_with_remove_duplicates(): + sub = Subtitle("tests/sub_with_styles.ass", remove_duplicates=True, exclude_styles=["Signs"]) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + assert len(dialogues) == 22 + + +def test_style_filtering_empty_include_list(sub_with_styles): + dialogues_normal = sub_with_styles.export(output_dialogues=True) + + sub = Subtitle("tests/sub_with_styles.ass", include_styles=[]) + dialogues = sub.export(output_dialogues=True) + + # Empty include list is treated as None (no filtering) + assert len(dialogues) == len(dialogues_normal) + assert len(dialogues_normal) == 25 + + +def test_style_filtering_empty_exclude_list(sub_with_styles): + dialogues_normal = sub_with_styles.export(output_dialogues=True) + + sub_filtered = Subtitle("tests/sub_with_styles.ass", exclude_styles=[]) + dialogues_filtered = sub_filtered.export(output_dialogues=True) + + # Empty exclude list should not filter anything + assert len(dialogues_filtered) == len(dialogues_normal) + assert len(dialogues_filtered) == 25 + + +@pytest.mark.parametrize( + "include_styles, exclude_styles, expected_count", + [ + (["Default"], None, 12), + (None, ["Signs"], 22), + (["Default", "Alt"], None, 15), + (None, ["Signs", "Credits"], 21), + ], +) +def test_style_filtering_combinations(include_styles, exclude_styles, expected_count): + sub = Subtitle("tests/sub_with_styles.ass", include_styles=include_styles, exclude_styles=exclude_styles) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + assert len(dialogues) == expected_count + + +def test_only_default_style_flag(): + # Test only_default_style flag keeps only styles with "Default" in name + sub = Subtitle("tests/sub_with_styles.ass", only_default_style=True) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + # Should keep only "Default" style (12 dialogues) + assert len(dialogues) == 12 + + +def test_only_default_style_with_explicit_include(): + # If include_styles is explicitly set, only_default_style should not override it + sub = Subtitle("tests/sub_with_styles.ass", only_default_style=True, include_styles=["Default"]) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + assert len(dialogues) == 12 + + +def test_only_default_style_with_removing_effects(): + sub = Subtitle("tests/sub_with_styles.ass", only_default_style=True, removing_effects=True) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + assert len(dialogues) == 12 + + +def test_only_default_style_with_remove_duplicates(): + sub = Subtitle("tests/sub_with_styles.ass", only_default_style=True, remove_duplicates=True) + dialogues = sub.export(output_dialogues=True) + + assert isinstance(dialogues, list) + assert len(dialogues) == 12 + + +def test_get_styles(sub_with_styles): + styles = sub_with_styles.get_styles() + + assert isinstance(styles, list) + assert len(styles) == 6 + assert "Default" in styles + assert "Alt" in styles + assert "Thoughts" in styles + assert "Top" in styles + assert "Signs" in styles + assert "Credits" in styles + + +def test_get_styles_sorted(sub_with_styles): + styles = sub_with_styles.get_styles() + + # Styles should be returned in alphabetical order + assert styles == sorted(styles) + + +def test_get_styles_srt_format(sub_srt): + styles = sub_srt.get_styles() + + # SRT files don't have styles + assert styles == [] From aaf1385eb45e3000361de5c141d2187b72977f2d Mon Sep 17 00:00:00 2001 From: GitBib <15717621+GitBib@users.noreply.github.com> Date: Thu, 13 Nov 2025 00:59:11 +0300 Subject: [PATCH 2/5] Update Python version constraints and dependency resolutions - Adjusted `requires-python` to `>=3.9` in `uv.lock` for newer Python versions. - Updated resolution markers for compatibility with Python 3.14. - Removed support for Python <3.9 across dependencies. - Cleaned up outdated wheel links and redundant dependencies. --- .github/workflows/python-test.yml | 2 +- pyproject.toml | 9 +- uv.lock | 281 ++++++++---------------------- 3 files changed, 75 insertions(+), 217 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 4b4bea8..d074ead 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 23d6eff..51231f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "pyasstosrt" version = "1.4.3" description = "Convert ASS subtitle to SRT format" authors = [{ name = "GitBib", email = "me@bnff.website" }] -requires-python = ">=3.8" +requires-python = ">=3.9" readme = "README.md" license = { text = "Apache License, Version 2.0" } keywords = [ @@ -26,7 +26,6 @@ keywords = [ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -51,12 +50,12 @@ pyasstosrt = "pyasstosrt.batch:app" [dependency-groups] dev = [ "pytest>=7.0.0,<8.0.0 ; python_version < '3.12'", - "pytest>=9.0.0 ; python_version >= '3.12'", + "pytest>=9.0.1 ; python_version >= '3.12'", "pytest-cov>=4.1.0,<5.0.0 ; python_version < '3.12'", "pytest-cov>=7.0.0 ; python_version >= '3.12'", "ruff>=0.14.4", "Sphinx>=8.0.2 ; python_version >= '3.12' and python_version < '3.14'", - "sphinx-immaterial>=0.13.6 ; python_version >= '3.12' and python_version < '3.14'", + "sphinx-immaterial>=0.13.8 ; python_version >= '3.12' and python_version < '3.14'", "tomli>=2.3.0", ] @@ -133,7 +132,7 @@ all = true include = ["pyasstosrt", "tests"] [tool.ruff] -target-version = "py38" +target-version = "py313" line-length = 120 [tool.ruff.lint] diff --git a/uv.lock b/uv.lock index 6928029..09edb61 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,11 @@ version = 1 revision = 3 -requires-python = ">=3.8" +requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] [[package]] @@ -114,17 +114,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/63a45bfc36f73efe46731a3a71cb84e2112f7e0b049507025ce477f0f052/charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", size = 198805, upload-time = "2025-08-09T07:56:56.496Z" }, - { url = "https://files.pythonhosted.org/packages/0c/52/8b0c6c3e53f7e546a5e49b9edb876f379725914e1130297f3b423c7b71c5/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", size = 142862, upload-time = "2025-08-09T07:56:57.751Z" }, - { url = "https://files.pythonhosted.org/packages/59/c0/a74f3bd167d311365e7973990243f32c35e7a94e45103125275b9e6c479f/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", size = 155104, upload-time = "2025-08-09T07:56:58.984Z" }, - { url = "https://files.pythonhosted.org/packages/1a/79/ae516e678d6e32df2e7e740a7be51dc80b700e2697cb70054a0f1ac2c955/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", size = 152598, upload-time = "2025-08-09T07:57:00.201Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/ef9c88464b126fa176f4ef4a317ad9b6f4d30b2cffbc43386062367c3e2c/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", size = 147391, upload-time = "2025-08-09T07:57:01.441Z" }, - { url = "https://files.pythonhosted.org/packages/7a/03/cbb6fac9d3e57f7e07ce062712ee80d80a5ab46614684078461917426279/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", size = 145037, upload-time = "2025-08-09T07:57:02.638Z" }, - { url = "https://files.pythonhosted.org/packages/64/d1/f9d141c893ef5d4243bc75c130e95af8fd4bc355beff06e9b1e941daad6e/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", size = 156425, upload-time = "2025-08-09T07:57:03.898Z" }, - { url = "https://files.pythonhosted.org/packages/c5/35/9c99739250742375167bc1b1319cd1cec2bf67438a70d84b2e1ec4c9daa3/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", size = 153734, upload-time = "2025-08-09T07:57:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/50/10/c117806094d2c956ba88958dab680574019abc0c02bcf57b32287afca544/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", size = 148551, upload-time = "2025-08-09T07:57:06.823Z" }, - { url = "https://files.pythonhosted.org/packages/61/c5/dc3ba772489c453621ffc27e8978a98fe7e41a93e787e5e5bde797f1dddb/charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", size = 98459, upload-time = "2025-08-09T07:57:08.031Z" }, - { url = "https://files.pythonhosted.org/packages/05/35/bb59b1cd012d7196fc81c2f5879113971efc226a63812c9cf7f89fe97c40/charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", size = 105887, upload-time = "2025-08-09T07:57:09.401Z" }, { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, @@ -144,8 +133,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, @@ -160,7 +148,8 @@ name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version >= '3.10' and python_full_version < '3.12'", ] dependencies = [ @@ -180,102 +169,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "coverage" -version = "7.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, - { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, - { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, - { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, - { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, - { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, - { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, - { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, - { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, - { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, - { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, - { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, - { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, - { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, - { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, - { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, - { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, - { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, - { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, - { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, - { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, - { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, - { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, - { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, - { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, - { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, - { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, - { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, - { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, - { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, - { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, - { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, - { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, - { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, - { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, - { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, - { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version < '3.9'" }, -] - [[package]] name = "coverage" version = "7.10.6" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, @@ -369,7 +266,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -386,8 +283,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -426,7 +322,7 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe", marker = "python_full_version >= '3.12'" }, + { name = "markupsafe", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ @@ -438,8 +334,7 @@ name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] dependencies = [ { name = "mdurl", marker = "python_full_version < '3.10'" }, @@ -454,7 +349,8 @@ name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version >= '3.10' and python_full_version < '3.12'", ] dependencies = [ @@ -551,27 +447,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, @@ -590,7 +469,7 @@ cli = [ [package.dev-dependencies] dev = [ { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "pytest", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "ruff" }, @@ -606,12 +485,12 @@ provides-extras = ["cli"] [package.metadata.requires-dev] dev = [ { name = "pytest", marker = "python_full_version < '3.12'", specifier = ">=7.0.0,<8.0.0" }, - { name = "pytest", marker = "python_full_version >= '3.12'", specifier = ">=9.0.0" }, + { name = "pytest", marker = "python_full_version >= '3.12'", specifier = ">=9.0.1" }, { name = "pytest-cov", marker = "python_full_version < '3.12'", specifier = ">=4.1.0,<5.0.0" }, { name = "pytest-cov", marker = "python_full_version >= '3.12'", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.14.4" }, { name = "sphinx", marker = "python_full_version >= '3.12' and python_full_version < '3.14'", specifier = ">=8.0.2" }, - { name = "sphinx-immaterial", marker = "python_full_version >= '3.12' and python_full_version < '3.14'", specifier = ">=0.13.6" }, + { name = "sphinx-immaterial", marker = "python_full_version >= '3.12' and python_full_version < '3.14'", specifier = ">=0.13.8" }, { name = "tomli", specifier = ">=2.3.0" }, ] @@ -620,10 +499,10 @@ name = "pydantic" version = "2.11.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "python_full_version >= '3.12'" }, - { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, + { name = "annotated-types", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } wheels = [ @@ -635,7 +514,7 @@ name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ @@ -744,8 +623,8 @@ name = "pydantic-extra-types" version = "2.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/ba/4178111ec4116c54e1dc7ecd2a1ff8f54256cdbd250e576882911e8f710a/pydantic_extra_types-2.10.5.tar.gz", hash = "sha256:1dcfa2c0cf741a422f088e0dbb4690e7bfadaaf050da3d6f80d6c3cf58a2bad8", size = 138429, upload-time = "2025-06-02T09:31:52.713Z" } wheels = [ @@ -767,16 +646,14 @@ version = "7.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig", marker = "python_full_version < '3.12'" }, { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.12'" }, + { name = "pluggy", marker = "python_full_version < '3.12'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } @@ -786,21 +663,22 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.0" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "iniconfig", marker = "python_full_version >= '3.12'" }, { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pluggy", marker = "python_full_version >= '3.12'" }, { name = "pygments", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] [[package]] @@ -809,12 +687,10 @@ version = "4.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] dependencies = [ - { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, - { name = "coverage", version = "7.10.6", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.9' and python_full_version < '3.12'" }, + { name = "coverage", extra = ["toml"], marker = "python_full_version < '3.12'" }, { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } @@ -827,12 +703,13 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", ] dependencies = [ - { name = "coverage", version = "7.10.6", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.12'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pytest", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "coverage", extra = ["toml"], marker = "python_full_version >= '3.12'" }, + { name = "pluggy", marker = "python_full_version >= '3.12'" }, + { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ @@ -844,10 +721,10 @@ name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "charset-normalizer", marker = "python_full_version >= '3.12'" }, - { name = "idna", marker = "python_full_version >= '3.12'" }, - { name = "urllib3", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "idna", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "urllib3", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ @@ -926,23 +803,23 @@ name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "babel", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "imagesize", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "jinja2", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pygments", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ @@ -951,19 +828,19 @@ wheels = [ [[package]] name = "sphinx-immaterial" -version = "0.13.6" +version = "0.13.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appdirs", marker = "python_full_version >= '3.12'" }, - { name = "markupsafe", marker = "python_full_version >= '3.12'" }, - { name = "pydantic", marker = "python_full_version >= '3.12'" }, - { name = "pydantic-extra-types", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "sphinx", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "appdirs", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "markupsafe", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pydantic-extra-types", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sphinx", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/11/cabf2ba4f0ca757e3c7d93917afc623597c60ae41bc6b52dce3c278bb0e0/sphinx_immaterial-0.13.6-py3-none-any.whl", hash = "sha256:4c7dce949967fa0905f71e5af22ba781f2dc9a22f2c6d4e0cef166cef099871c", size = 11409972, upload-time = "2025-08-14T00:15:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/23/43/06097b237575e80576003cde825f7254affe9bd13df88a54a7a3da964bcb/sphinx_immaterial-0.13.8-py3-none-any.whl", hash = "sha256:fd92cc9e9f65e1f3a28a9f4f6886250a97dc65da41ece67f562bc1c00e89be6f", size = 11410697, upload-time = "2025-10-13T19:57:33.489Z" }, ] [[package]] @@ -1078,35 +955,17 @@ dependencies = [ { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, @@ -1117,7 +976,7 @@ name = "typing-inspection" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ From 78a7ca808dee091cf395aff66724c7f0dd1675d9 Mon Sep 17 00:00:00 2001 From: GitBib <15717621+GitBib@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:05:31 +0300 Subject: [PATCH 3/5] Update GitHub workflows: adjust branch triggers and permissions - Removed `main` branch trigger from `python-docs.yml`. - Added explicit `master` branch triggers to `python-linter.yml` and `python-test.yml`. --- .github/workflows/python-docs.yml | 1 - .github/workflows/python-linter.yml | 2 ++ .github/workflows/python-test.yml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-docs.yml b/.github/workflows/python-docs.yml index 59d9c5d..88128d8 100644 --- a/.github/workflows/python-docs.yml +++ b/.github/workflows/python-docs.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - main workflow_dispatch: permissions: diff --git a/.github/workflows/python-linter.yml b/.github/workflows/python-linter.yml index 4cb36a6..1b9f6dc 100644 --- a/.github/workflows/python-linter.yml +++ b/.github/workflows/python-linter.yml @@ -3,6 +3,8 @@ name: Python Linter on: pull_request: push: + branches: + - master permissions: contents: read diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index d074ead..737fa1d 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -3,6 +3,8 @@ name: Run Python Tests on: pull_request: push: + branches: + - master permissions: contents: read From 1b1d49209b97632f913ecb84cc2407e7c22fcb4b Mon Sep 17 00:00:00 2001 From: GitBib <15717621+GitBib@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:14:03 +0300 Subject: [PATCH 4/5] Add extended test coverage for style filtering and CLI commands - Introduced tests for `--only-default`, `--include-styles`, and `--exclude-styles` export options. - Verified mutually exclusive style flag behavior with test cases. - Added tests for `styles` command output in simple list and table formats. - Expanded error handling tests for permission errors, unexpected runtime errors, and non-existent files in both export and styles commands. - Updated test fixtures to include `sub_with_styles` file. --- tests/conftest.py | 1 + tests/test_batch.py | 124 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 5bf629a..23cffd9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,6 +42,7 @@ def test_files(test_dir): "sub_removing_effects": test_dir / "sub-removing-effects.ass", "sub_standard": test_dir / "sub_standard.srt", "sub_standard_removing_effects": test_dir / "sub_standard-removing-effects.srt", + "sub_with_styles": test_dir / "sub_with_styles.ass", } diff --git a/tests/test_batch.py b/tests/test_batch.py index 88b62cf..a60a5fa 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -256,3 +256,127 @@ def test_export_srt_with_output_dialogues_cli(cli_runner, test_dir): assert f"Dialogues for {test_file.name}:" in result.stdout assert "It's time for the main event!" in result.stdout + + +def test_export_with_only_default_style(cli_runner, test_files, cleanup_srt_files): + """Test exporting with --only-default flag.""" + test_file = test_files["sub"] + srt_file = test_file.with_suffix(".srt") + + result = cli_runner.invoke(app, ["export", str(test_file), "--only-default"]) + assert result.exit_code == 0 + assert "Filter: Only 'Default' styles" in result.stdout + assert srt_file.exists() + + +def test_export_with_include_styles(cli_runner, test_files, cleanup_srt_files): + """Test exporting with --include-styles flag.""" + test_file = test_files["sub"] + srt_file = test_file.with_suffix(".srt") + + result = cli_runner.invoke(app, ["export", str(test_file), "--include-styles", "Default,Signs"]) + assert result.exit_code == 0 + assert "Filter: Include styles: Default,Signs" in result.stdout + assert srt_file.exists() + + +def test_export_with_exclude_styles(cli_runner, test_files, cleanup_srt_files): + """Test exporting with --exclude-styles flag.""" + test_file = test_files["sub"] + srt_file = test_file.with_suffix(".srt") + + result = cli_runner.invoke(app, ["export", str(test_file), "--exclude-styles", "Signs,Credits"]) + assert result.exit_code == 0 + assert "Filter: Exclude styles: Signs,Credits" in result.stdout + assert srt_file.exists() + + +def test_export_with_mutually_exclusive_styles(cli_runner, test_files): + """Test that mutually exclusive style options are rejected.""" + test_file = test_files["sub"] + + # Test --only-default with --include-styles + result = cli_runner.invoke(app, ["export", str(test_file), "--only-default", "--include-styles", "Default"]) + assert result.exit_code == 1 + assert "mutually exclusive" in result.stdout + + # Test --only-default with --exclude-styles + result = cli_runner.invoke(app, ["export", str(test_file), "--only-default", "--exclude-styles", "Signs"]) + assert result.exit_code == 1 + assert "mutually exclusive" in result.stdout + + # Test --include-styles with --exclude-styles + result = cli_runner.invoke( + app, ["export", str(test_file), "--include-styles", "Default", "--exclude-styles", "Signs"] + ) + assert result.exit_code == 1 + assert "mutually exclusive" in result.stdout + + +def test_export_with_permission_error(cli_runner, test_files, monkeypatch): + """Test handling of PermissionError during export.""" + test_file = test_files["sub"] + + def mock_init(*args, **kwargs): + raise PermissionError("Permission denied") + + monkeypatch.setattr(OriginalSubtitle, "__init__", mock_init) + + result = cli_runner.invoke(app, ["export", str(test_file)]) + assert result.exit_code == 1 + assert "✗ Error:" in result.stdout + assert "Permission denied" in result.stdout + + +def test_styles_command_simple_list(cli_runner, test_files): + """Test styles command with simple list output.""" + test_file = test_files["sub_with_styles"] + + result = cli_runner.invoke(app, ["styles", str(test_file)]) + assert result.exit_code == 0 + assert "Analyzing styles in:" in result.stdout + assert "Styles found in" in result.stdout + assert "Total:" in result.stdout + assert "Tips:" in result.stdout + # Check both Default and non-Default styles are shown + assert "Default" in result.stdout + assert "Signs" in result.stdout or "Alt" in result.stdout + + +def test_styles_command_table_format(cli_runner, test_files): + """Test styles command with table format.""" + test_file = test_files["sub"] + + result = cli_runner.invoke(app, ["styles", str(test_file), "--table"]) + assert result.exit_code == 0 + assert "Analyzing styles in:" in result.stdout + assert "Styles in" in result.stdout + + +def test_styles_command_file_not_found(cli_runner): + """Test styles command with non-existent file.""" + result = cli_runner.invoke(app, ["styles", "nonexistent.ass"]) + assert result.exit_code != 0 + + +def test_styles_command_with_error(cli_runner, test_files, monkeypatch): + """Test styles command error handling.""" + test_file = test_files["sub"] + + def mock_init(*args, **kwargs): + raise RuntimeError("Test error") + + monkeypatch.setattr(OriginalSubtitle, "__init__", mock_init) + + result = cli_runner.invoke(app, ["styles", str(test_file)]) + assert result.exit_code == 1 + assert "✗ Error:" in result.stdout + + +def test_styles_command_with_srt_file(cli_runner, test_dir): + """Test styles command with SRT file (no styles).""" + test_file = test_dir / "test_sample.srt" + + result = cli_runner.invoke(app, ["styles", str(test_file)]) + assert result.exit_code == 0 + assert ("No styles found" in result.stdout) or ("file is in SRT format" in result.stdout) From 47baf7379bc21431fa5ed354eef434975ac902de Mon Sep 17 00:00:00 2001 From: GitBib <15717621+GitBib@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:17:09 +0300 Subject: [PATCH 5/5] Expand test suite to handle `FileNotFoundError` in `export` and `styles` commands - Added tests to verify error handling for `FileNotFoundError` in `export` and `styles` commands. - Simulated missing file scenarios using `monkeypatch` to mock `OriginalSubtitle` initialization failure. - Validated CLI outputs for appropriate error messages. --- tests/test_batch.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_batch.py b/tests/test_batch.py index a60a5fa..da4b337 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -328,6 +328,21 @@ def mock_init(*args, **kwargs): assert "Permission denied" in result.stdout +def test_export_with_file_not_found_error(cli_runner, test_files, monkeypatch): + """Test handling of FileNotFoundError during export.""" + test_file = test_files["sub"] + + def mock_init(*args, **kwargs): + raise FileNotFoundError("File disappeared") + + monkeypatch.setattr(OriginalSubtitle, "__init__", mock_init) + + result = cli_runner.invoke(app, ["export", str(test_file)]) + assert result.exit_code == 1 + assert "✗ Error:" in result.stdout + assert "File not found" in result.stdout + + def test_styles_command_simple_list(cli_runner, test_files): """Test styles command with simple list output.""" test_file = test_files["sub_with_styles"] @@ -373,6 +388,21 @@ def mock_init(*args, **kwargs): assert "✗ Error:" in result.stdout +def test_styles_command_with_file_not_found_error(cli_runner, test_files, monkeypatch): + """Test styles command FileNotFoundError handling.""" + test_file = test_files["sub"] + + def mock_init(*args, **kwargs): + raise FileNotFoundError("File disappeared") + + monkeypatch.setattr(OriginalSubtitle, "__init__", mock_init) + + result = cli_runner.invoke(app, ["styles", str(test_file)]) + assert result.exit_code == 1 + assert "✗ Error:" in result.stdout + assert "File not found" in result.stdout + + def test_styles_command_with_srt_file(cli_runner, test_dir): """Test styles command with SRT file (no styles).""" test_file = test_dir / "test_sample.srt"