diff --git a/.cursor/index.mdc b/.cursor/index.mdc deleted file mode 100644 index 5b9200b..0000000 --- a/.cursor/index.mdc +++ /dev/null @@ -1,49 +0,0 @@ ---- -alwaysApply: true ---- - -# repo-to-text - -## Project Overview -`repo-to-text` is a command-line tool that converts a directory's structure and contents into a single text file. -It generates a formatted XML representation that includes the directory tree and file contents, making it easy to share code with LLMs for development and debugging. - -## Usage -- Install: `pip install repo-to-text` -- Run: `cd && repo-to-text` -- The result will be saved in the current directory as `repo-to-text_YYYY-MM-DD-HH-MM-SS-UTC.txt` - -## Common Commands -- `repo-to-text` - Process current directory -- `repo-to-text /path/to/dir` - Process specific directory -- `repo-to-text --output-dir /path/to/output` - Specify output directory -- `repo-to-text --stdout > myfile.txt` - Output to stdout and redirect to file -- `repo-to-text --create-settings` - Create a default settings file - -## Output Format -The tool generates an XML-structured output with: -- Root `` tag -- Directory structure in `` tags -- File contents in `` tags - -## Configuration -- Create `.repo-to-text-settings.yaml` at the root of your project -- Use gitignore-style rules to specify what files to ignore -- Configure what files to include in the tree and content sections - -## Development -- Python >= 3.6 -- Install dev dependencies: `pip install -e ".[dev]"` -- Run tests: `pytest` - -## Testing -- Tests are located in the `tests/` directory -- Main test files: - - `tests/test_core.py` - Tests for core functionality - - `tests/test_cli.py` - Tests for command-line interface - - `tests/test_utils.py` - Tests for utility functions -- Run all tests: `pytest` -- Run specific test file: `pytest tests/test_core.py` -- Run with coverage: `pytest --cov=repo_to_text` -- Test temporary directories are created and cleaned up automatically -- Binary file handling is tested with mock binary data \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6abbfa8..4c6e38e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.11", "3.13"] + python-version: ["3.8", "3.11", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/.repo-to-text-settings.yaml b/.repo-to-text-settings.yaml index 8967240..889bb03 100644 --- a/.repo-to-text-settings.yaml +++ b/.repo-to-text-settings.yaml @@ -6,20 +6,15 @@ gitignore-import-and-ignore: True # Ignore files and directories for tree -# and contents sections (...) +# and "Contents of ..." sections ignore-tree-and-content: - ".repo-to-text-settings.yaml" - "examples/" - "MANIFEST.in" - "setup.py" -# Ignore files and directories for contents sections +# Ignore files and directories for "Contents of ..." section ignore-content: - "README.md" - "LICENSE" - "tests/" - -# Optional: Maximum number of words per output file before splitting. -# If not specified or null, no splitting based on word count will occur. -# Must be a positive integer if set. -# maximum_word_count_per_file: 10000 diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 94443be..0000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -.cursor/index.mdc \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 94443be..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -.cursor/index.mdc \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..a46a36d --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,27 @@ +# Docker Usage Instructions + +## Building and Running + +1. Build the container: +```bash +docker-compose build +``` + +2. Start a shell session: +```bash +docker-compose run --rm repo-to-text +``` + +Once in the shell, you can run repo-to-text: +```bash +# Process current directory +repo-to-text + +# Process specific directory +repo-to-text /home/user/myproject + +# Use with options +repo-to-text --output-dir /home/user/output +``` + +The container mounts your home directory at `/home/user`, allowing access to all your projects. diff --git a/Dockerfile b/Dockerfile index 8b5cd53..c142ab8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,13 @@ -FROM python:3.12-slim +FROM python:3.11-slim -# Set environment variables -ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=1 - -# Create non-root user -RUN useradd -m -s /bin/bash user - -# Install system dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt-get update && apt-get install -y \ tree \ && rm -rf /var/lib/apt/lists/* +RUN useradd -m -s /bin/bash user + WORKDIR /app - -# Copy all necessary files for package installation -COPY pyproject.toml README.md ./ - -# Copy the package source -COPY repo_to_text ./repo_to_text - -# Install the package -RUN pip install --no-cache-dir -e . - -# Copy remaining files COPY . . - -# Set default user -USER user +RUN pip install -e . && pip install pyperclip ENTRYPOINT ["repo-to-text"] diff --git a/README.md b/README.md index c9e730e..09fcf8f 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,22 @@ # Repository to Text Conversion: repo-to-text command -`repo-to-text` converts a directory's structure and contents into a single text file. Run it from the terminal to generate a formatted text representation that includes the directory tree and file contents. This makes it easy to share code with LLMs for development and debugging. - -## Quick Start - -1. `pip install repo-to-text` — install the package -2. `cd ` — navigate to the repository directory -3. `repo-to-text` — run the command, result will be saved in the current directory +`repo-to-text` is an open-source project that converts the structure and contents of a directory (repository) into a single text file. By executing a simple command in the terminal, this tool generates a text representation of the directory, including the output of the `tree` command and the contents of each file, formatted for easy reading and sharing. This can be very useful for development and debugging with LLM. ## Example of Repository to Text Conversion ![Example Output](https://raw.githubusercontent.com/kirill-markin/repo-to-text/main/examples/screenshot-demo.jpg) -The generated text file will include the directory structure and contents of each file, using XML tags for better structure: +The generated text file will include the directory structure and contents of each file. For a full example, see the [example output for this repository](https://github.com/kirill-markin/repo-to-text/blob/main/examples/example_repo-to-text_2024-06-09-08-06-31-UTC.txt). -```xml - -Directory: myproject +The same text will appear in your clipboard. You can paste it into a dialog with the LLM and start communicating. -Directory Structure: - -. -├── .gitignore -├── README.md -└── src - └── main.py - +## Features - -# My Project -This is a simple project. - - - -def main(): - print("Hello, World!") - -if __name__ == "__main__": - main() - - - - -For a full example, see the [example output for this repository](https://github.com/kirill-markin/repo-to-text/blob/main/examples/example_repo-to-text_2024-06-09-08-06-31-UTC.txt). +- Generates a text representation of a directory's structure. +- Includes the output of the `tree` command. +- Saves the contents of each file, encapsulated in markdown code blocks. +- Copies the generated text representation to the clipboard for easy sharing. +- Easy to install and use via `pip`. ## Installation @@ -128,44 +102,6 @@ You can customize the behavior of `repo-to-text` with the following options: This will write the output directly to `myfile.txt` instead of creating a timestamped file. -## Docker Usage - -### Building and Running - -1. Build the container: - - ```bash - docker compose build - ``` - -2. Start a shell session: - - ```bash - docker compose run --rm repo-to-text - ``` - -Once in the shell, you can run `repo-to-text`: - -- Process current directory: - - ```bash - repo-to-text - ``` - -- Process specific directory: - - ```bash - repo-to-text /home/user/myproject - ``` - -- Use with options: - - ```bash - repo-to-text --output-dir /home/user/output - ``` - -The container mounts your home directory at `/home/user`, allowing access to all your projects. - ## Settings `repo-to-text` also supports configuration via a `.repo-to-text-settings.yaml` file. By default, the tool works without this file, but you can use it to customize what gets included in the final text file. @@ -182,14 +118,14 @@ To create a settings file, add a file named `.repo-to-text-settings.yaml` at the gitignore-import-and-ignore: True # Ignore files and directories for tree -# and contents sections (...) +# and "Contents of ..." sections ignore-tree-and-content: - ".repo-to-text-settings.yaml" - "examples/" - "MANIFEST.in" - "setup.py" -# Ignore files and directories for contents sections +# Ignore files and directories for "Contents of ..." section ignore-content: - "README.md" - "LICENSE" @@ -201,17 +137,10 @@ You can copy this file from the [existing example in the project](https://github ### Configuration Options - **gitignore-import-and-ignore**: Ignore files and directories specified in `.gitignore` for all sections. -- **ignore-tree-and-content**: Ignore files and directories for the tree and contents sections. -- **ignore-content**: Ignore files and directories only for the contents sections. +- **ignore-tree-and-content**: Ignore files and directories for the tree and "Contents of ..." sections. +- **ignore-content**: Ignore files and directories only for the "Contents of ..." section. Using these settings, you can control which files and directories are included or excluded from the final text file. -- **maximum_word_count_per_file**: Optional integer. Sets a maximum word count for each output file. If the total content exceeds this limit, the output will be split into multiple files. The split files will be named using the convention `output_filename_part_N.txt`, where `N` is the part number. - Example: - ```yaml - # Optional: Maximum word count per output file. - # If set, the output will be split into multiple files if the total word count exceeds this. - # maximum_word_count_per_file: 10000 - ``` ### Wildcards and Inclusions diff --git a/docker-compose.yaml b/docker-compose.yaml index 07a8e37..75d0efc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,10 @@ services: repo-to-text: - build: - context: . - dockerfile: Dockerfile + build: . volumes: - ${HOME:-/home/user}:/home/user working_dir: /home/user environment: - HOME=/home/user user: "${UID:-1000}:${GID:-1000}" - init: true entrypoint: ["/bin/bash"] diff --git a/examples/example_repo-to-text_2024-06-09-08-06-31-UTC.txt b/examples/example_repo-to-text_2024-06-09-08-06-31-UTC.txt index d3be78d..eb74a03 100644 --- a/examples/example_repo-to-text_2024-06-09-08-06-31-UTC.txt +++ b/examples/example_repo-to-text_2024-06-09-08-06-31-UTC.txt @@ -1,689 +1,225 @@ - Directory: repo-to-text Directory Structure: - +``` . ├── .gitignore -├── .cursorignore -├── Dockerfile ├── LICENSE ├── README.md -├── docker-compose.yaml -├── pyproject.toml +├── repo_to_text │   ├── repo_to_text/__init__.py -│   ├── repo_to_text/cli -│   │   ├── repo_to_text/cli/__init__.py -│   │   └── repo_to_text/cli/cli.py -│   ├── repo_to_text/core -│   │   ├── repo_to_text/core/__init__.py -│   │   └── repo_to_text/core/core.py -│   ├── repo_to_text/main.py -│   └── repo_to_text/utils -│   ├── repo_to_text/utils/__init__.py -│   └── repo_to_text/utils/utils.py +│   └── repo_to_text/main.py +├── requirements.txt +└── tests ├── tests/__init__.py - ├── tests/test_cli.py - ├── tests/test_core.py - └── tests/test_utils.py - + └── tests/test_main.py +``` - -examples/* +Contents of requirements.txt: +``` +setuptools==70.0.0 +pathspec==0.12.1 +pytest==8.2.2 +argparse==1.4.0 +pyperclip==1.8.2 +PyYAML==6.0.1 - - - -services: - repo-to-text: - build: - context: . - dockerfile: Dockerfile - volumes: - - ${HOME:-/home/user}:/home/user - working_dir: /home/user - environment: - - HOME=/home/user - user: "${UID:-1000}:${GID:-1000}" - init: true - entrypoint: ["/bin/bash"] - - - - -FROM python:3.12-slim - -# Set environment variables -ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=1 - -# Create non-root user -RUN useradd -m -s /bin/bash user - -# Install system dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - tree \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -# Copy all necessary files for package installation -COPY pyproject.toml README.md ./ - -# Copy the package source -COPY repo_to_text ./repo_to_text - -# Install the package -RUN pip install --no-cache-dir -e . - -# Copy remaining files -COPY . . - -# Set default user -USER user - -ENTRYPOINT ["repo-to-text"] - - - - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "repo-to-text" -version = "0.5.4" -authors = [ - { name = "Kirill Markin", email = "markinkirill@gmail.com" }, -] -description = "Convert a directory structure and its contents into a single text file, including the tree output and file contents in markdown code blocks. It may be useful to chat with LLM about your code." -readme = "README.md" -requires-python = ">=3.6" -license = { text = "MIT" } -classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Development Status :: 4 - Beta", -] -dependencies = [ - "setuptools>=70.0.0", - "pathspec>=0.12.1", - "argparse>=1.4.0", - "PyYAML>=6.0.1", -] - -[project.urls] -Homepage = "https://github.com/kirill-markin/repo-to-text" -Repository = "https://github.com/kirill-markin/repo-to-text" - -[project.scripts] -repo-to-text = "repo_to_text.main:main" -flatten = "repo_to_text.main:main" - -[project.optional-dependencies] -dev = [ - "pytest>=8.2.2", - "black", - "mypy", - "isort", - "build", - "twine", - "pylint", -] - -[tool.pylint] -disable = [ - "C0303", -] - - - - -"""This is the main package for the repo_to_text package.""" +``` +Contents of repo_to_text/__init__.py: +``` __author__ = 'Kirill Markin' __email__ = 'markinkirill@gmail.com' - - - -"""This is the main entry point for the repo_to_text package.""" - -from repo_to_text.cli.cli import main - -if __name__ == '__main__': - main() - - - - -"""This module contains the core functionality of the repo_to_text package.""" - -from .core import get_tree_structure, load_ignore_specs, should_ignore_file, save_repo_to_text - -__all__ = ['get_tree_structure', 'load_ignore_specs', 'should_ignore_file', 'save_repo_to_text'] - - - - -""" -Core functionality for repo-to-text -""" +``` +Contents of repo_to_text/main.py: +``` import os import subprocess -from typing import Tuple, Optional, List, Dict, Any, Set -from datetime import datetime, timezone -from importlib.machinery import ModuleSpec -import logging -import yaml import pathspec -from pathspec import PathSpec +import logging +import argparse +import yaml +from datetime import datetime +import pyperclip -from ..utils.utils import check_tree_command, is_ignored_path +def setup_logging(debug=False): + logging_level = logging.DEBUG if debug else logging.INFO + logging.basicConfig(level=logging_level, format='%(asctime)s - %(levelname)s - %(message)s') -def get_tree_structure( - path: str = '.', - gitignore_spec: Optional[PathSpec] = None, - tree_and_content_ignore_spec: Optional[PathSpec] = None - ) -> str: - """Generate tree structure of the directory.""" - if not check_tree_command(): - return "" - - logging.debug('Generating tree structure for path: %s', path) - tree_output = run_tree_command(path) - logging.debug('Tree output generated:\n%s', tree_output) +def get_tree_structure(path='.', gitignore_spec=None, tree_and_content_ignore_spec=None) -> str: + logging.debug(f'Generating tree structure for path: {path}') + result = subprocess.run(['tree', '-a', '-f', '--noreport', path], stdout=subprocess.PIPE) + tree_output = result.stdout.decode('utf-8') + logging.debug(f'Tree output generated: {tree_output}') if not gitignore_spec and not tree_and_content_ignore_spec: logging.debug('No .gitignore or ignore-tree-and-content specification found') return tree_output - logging.debug('Filtering tree output based on ignore specifications') - return filter_tree_output(tree_output, path, gitignore_spec, tree_and_content_ignore_spec) - -def run_tree_command(path: str) -> str: - """Run the tree command and return its output.""" - result = subprocess.run( - ['tree', '-a', '-f', '--noreport', path], - stdout=subprocess.PIPE, - check=True - ) - return result.stdout.decode('utf-8') - -def filter_tree_output( - tree_output: str, - path: str, - gitignore_spec: Optional[PathSpec], - tree_and_content_ignore_spec: Optional[PathSpec] - ) -> str: - """Filter the tree output based on ignore specifications.""" - lines: List[str] = tree_output.splitlines() - non_empty_dirs: Set[str] = set() - - filtered_lines = [ - process_line(line, path, gitignore_spec, tree_and_content_ignore_spec, non_empty_dirs) - for line in lines - ] - - filtered_tree_output = '\n'.join(filter(None, filtered_lines)) - logging.debug('Filtered tree structure:\n%s', filtered_tree_output) - return filtered_tree_output - -def process_line( - line: str, - path: str, - gitignore_spec: Optional[PathSpec], - tree_and_content_ignore_spec: Optional[PathSpec], - non_empty_dirs: Set[str] - ) -> Optional[str]: - """Process a single line of the tree output.""" - full_path = extract_full_path(line, path) - if not full_path or full_path == '.': - return None - - relative_path = os.path.relpath(full_path, path).replace(os.sep, '/') - - if should_ignore_file( - full_path, - relative_path, - gitignore_spec, - None, - tree_and_content_ignore_spec - ): - logging.debug('Ignored: %s', relative_path) - return None - - if not os.path.isdir(full_path): - mark_non_empty_dirs(relative_path, non_empty_dirs) - - if not os.path.isdir(full_path) or os.path.dirname(relative_path) in non_empty_dirs: - return line.replace('./', '', 1) - return None - -def extract_full_path(line: str, path: str) -> Optional[str]: - """Extract the full path from a line of tree output.""" - idx = line.find('./') - if idx == -1: - idx = line.find(path) - return line[idx:].strip() if idx != -1 else None - -def mark_non_empty_dirs(relative_path: str, non_empty_dirs: Set[str]) -> None: - """Mark all parent directories of a file as non-empty.""" - dir_path = os.path.dirname(relative_path) - while dir_path: - non_empty_dirs.add(dir_path) - dir_path = os.path.dirname(dir_path) - -def load_ignore_specs( - path: str = '.', - cli_ignore_patterns: Optional[List[str]] = None - ) -> Tuple[Optional[PathSpec], Optional[PathSpec], PathSpec]: - """Load ignore specifications from various sources. + logging.debug('Filtering tree output based on .gitignore and ignore-tree-and-content specification') + filtered_lines = [] + for line in tree_output.splitlines(): + parts = line.strip().split() + if parts: + full_path = parts[-1] + relative_path = os.path.relpath(full_path, path) + if not should_ignore_file(full_path, relative_path, gitignore_spec, None, tree_and_content_ignore_spec): + filtered_lines.append(line.replace('./', '', 1)) - Args: - path: Base directory path - cli_ignore_patterns: List of patterns from command line - - Returns: - Tuple[Optional[PathSpec], Optional[PathSpec], PathSpec]: Tuple of gitignore_spec, - content_ignore_spec, and tree_and_content_ignore_spec - """ + logging.debug('Tree structure filtering complete') + return '\n'.join(filtered_lines) + +def load_ignore_specs(path='.'): gitignore_spec = None content_ignore_spec = None - tree_and_content_ignore_list: List[str] = [] + tree_and_content_ignore_spec = None use_gitignore = True repo_settings_path = os.path.join(path, '.repo-to-text-settings.yaml') if os.path.exists(repo_settings_path): - logging.debug('Loading .repo-to-text-settings.yaml from path: %s', repo_settings_path) - with open(repo_settings_path, 'r', encoding='utf-8') as f: - settings: Dict[str, Any] = yaml.safe_load(f) + logging.debug(f'Loading .repo-to-text-settings.yaml from path: {repo_settings_path}') + with open(repo_settings_path, 'r') as f: + settings = yaml.safe_load(f) use_gitignore = settings.get('gitignore-import-and-ignore', True) if 'ignore-content' in settings: - content_ignore_spec: Optional[PathSpec] = pathspec.PathSpec.from_lines( - 'gitwildmatch', settings['ignore-content'] - ) + content_ignore_spec = pathspec.PathSpec.from_lines('gitwildmatch', settings['ignore-content']) if 'ignore-tree-and-content' in settings: - tree_and_content_ignore_list.extend(settings.get('ignore-tree-and-content', [])) - - if cli_ignore_patterns: - tree_and_content_ignore_list.extend(cli_ignore_patterns) + tree_and_content_ignore_spec = pathspec.PathSpec.from_lines('gitwildmatch', settings['ignore-tree-and-content']) if use_gitignore: gitignore_path = os.path.join(path, '.gitignore') if os.path.exists(gitignore_path): - logging.debug('Loading .gitignore from path: %s', gitignore_path) - with open(gitignore_path, 'r', encoding='utf-8') as f: + logging.debug(f'Loading .gitignore from path: {gitignore_path}') + with open(gitignore_path, 'r') as f: gitignore_spec = pathspec.PathSpec.from_lines('gitwildmatch', f) - tree_and_content_ignore_spec = pathspec.PathSpec.from_lines( - 'gitwildmatch', tree_and_content_ignore_list - ) return gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec -def should_ignore_file( - file_path: str, - relative_path: str, - gitignore_spec: Optional[PathSpec], - content_ignore_spec: Optional[PathSpec], - tree_and_content_ignore_spec: Optional[PathSpec] -) -> bool: - """Check if a file should be ignored based on various ignore specifications. - - Args: - file_path: Full path to the file - relative_path: Path relative to the repository root - gitignore_spec: PathSpec object for gitignore patterns - content_ignore_spec: PathSpec object for content ignore patterns - tree_and_content_ignore_spec: PathSpec object for tree and content ignore patterns - - Returns: - bool: True if file should be ignored, False otherwise - """ - relative_path = relative_path.replace(os.sep, '/') - - if relative_path.startswith('./'): - relative_path = relative_path[2:] - - if os.path.isdir(file_path): - relative_path += '/' - - result = ( +def should_ignore_file(file_path, relative_path, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec): + return ( is_ignored_path(file_path) or - bool( - gitignore_spec and - gitignore_spec.match_file(relative_path) - ) or - bool( - content_ignore_spec and - content_ignore_spec.match_file(relative_path) - ) or - bool( - tree_and_content_ignore_spec and - tree_and_content_ignore_spec.match_file(relative_path) - ) or + (gitignore_spec and gitignore_spec.match_file(relative_path)) or + (content_ignore_spec and content_ignore_spec.match_file(relative_path)) or + (tree_and_content_ignore_spec and tree_and_content_ignore_spec.match_file(relative_path)) or os.path.basename(file_path).startswith('repo-to-text_') ) - logging.debug('Checking if file should be ignored:') - logging.debug(' file_path: %s', file_path) - logging.debug(' relative_path: %s', relative_path) - logging.debug(' Result: %s', result) - return result - -def save_repo_to_text( - path: str = '.', - output_dir: Optional[str] = None, - to_stdout: bool = False, - cli_ignore_patterns: Optional[List[str]] = None - ) -> str: - """Save repository structure and contents to a text file.""" - logging.debug('Starting to save repo structure to text for path: %s', path) - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs( - path, cli_ignore_patterns - ) - tree_structure: str = get_tree_structure( - path, gitignore_spec, tree_and_content_ignore_spec - ) - logging.debug('Final tree structure to be written: %s', tree_structure) - - output_content = generate_output_content( - path, - tree_structure, - gitignore_spec, - content_ignore_spec, - tree_and_content_ignore_spec - ) - - if to_stdout: - print(output_content) - return output_content - - output_file = write_output_to_file(output_content, output_dir) - copy_to_clipboard(output_content) - - print( - "[SUCCESS] Repository structure and contents successfully saved to " - f"file: \"./{output_file}\"" - ) - - return output_file - -def generate_output_content( - path: str, - tree_structure: str, - gitignore_spec: Optional[PathSpec], - content_ignore_spec: Optional[PathSpec], - tree_and_content_ignore_spec: Optional[PathSpec] - ) -> str: - """Generate the output content for the repository.""" - output_content: List[str] = [] - project_name = os.path.basename(os.path.abspath(path)) - - # Add XML opening tag - output_content.append('\n') - - output_content.append(f'Directory: {project_name}\n\n') - output_content.append('Directory Structure:\n') - output_content.append('\n.\n') - - if os.path.exists(os.path.join(path, '.gitignore')): - output_content.append('├── .gitignore\n') - - output_content.append(tree_structure + '\n' + '\n') - logging.debug('Tree structure written to output content') - - for root, _, files in os.walk(path): - for filename in files: - file_path = os.path.join(root, filename) - relative_path = os.path.relpath(file_path, path) - - if should_ignore_file( - file_path, - relative_path, - gitignore_spec, - content_ignore_spec, - tree_and_content_ignore_spec - ): - continue - - relative_path = relative_path.replace('./', '', 1) - - try: - # Try to open as text first - with open(file_path, 'r', encoding='utf-8') as f: - file_content = f.read() - output_content.append(f'\n\n') - output_content.append(file_content) - output_content.append('\n\n') - except UnicodeDecodeError: - # Handle binary files with the same content tag format - logging.debug('Handling binary file contents: %s', file_path) - with open(file_path, 'rb') as f: - binary_content = f.read() - output_content.append(f'\n\n') - output_content.append(binary_content.decode('latin1')) - output_content.append('\n\n') - - # Add XML closing tag - output_content.append('\n\n') - - logging.debug('Repository contents written to output content') - - return ''.join(output_content) - -def write_output_to_file(output_content: str, output_dir: Optional[str]) -> str: - """Write the output content to a file.""" - timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d-%H-%M-%S-UTC') - output_file = f'repo-to-text_{timestamp}.txt' - - if output_dir: - if not os.path.exists(output_dir): - os.makedirs(output_dir) - output_file = os.path.join(output_dir, output_file) - - with open(output_file, 'w', encoding='utf-8') as file: - file.write(output_content) - - return output_file - -def copy_to_clipboard(output_content: str) -> None: - """Copy the output content to the clipboard if possible.""" - try: - import importlib.util # pylint: disable=import-outside-toplevel - spec: Optional[ModuleSpec] = importlib.util.find_spec("pyperclip") # type: ignore - if spec: - import pyperclip # pylint: disable=import-outside-toplevel # type: ignore - pyperclip.copy(output_content) # type: ignore - logging.debug('Repository structure and contents copied to clipboard') - else: - print("Tip: Install 'pyperclip' package to enable automatic clipboard copying:") - print(" pip install pyperclip") - except ImportError as e: - logging.warning( - 'Could not copy to clipboard. You might be running this ' - 'script over SSH or without clipboard support.' - ) - logging.debug('Clipboard copy error: %s', e) - - - - -"""This module contains utility functions for the repo_to_text package.""" - -from .utils import setup_logging, check_tree_command, is_ignored_path - -__all__ = ['setup_logging', 'check_tree_command', 'is_ignored_path'] - - - - -"""This module contains utility functions for the repo_to_text package.""" - -import shutil -import logging -from typing import List - -def setup_logging(debug: bool = False) -> None: - """Set up logging configuration. - - Args: - debug: If True, sets logging level to DEBUG, otherwise INFO - """ - logging_level = logging.DEBUG if debug else logging.INFO - logging.basicConfig(level=logging_level, format='%(asctime)s - %(levelname)s - %(message)s') - -def check_tree_command() -> bool: - """Check if the `tree` command is available, and suggest installation if not. - - Returns: - bool: True if tree command is available, False otherwise - """ - if shutil.which('tree') is None: - print( - "The 'tree' command is not found. " - + "Please install it using one of the following commands:" - ) - print("For Debian-based systems (e.g., Ubuntu): sudo apt-get install tree") - print("For Red Hat-based systems (e.g., Fedora, CentOS): sudo yum install tree") - return False - return True - def is_ignored_path(file_path: str) -> bool: - """Check if a file path should be ignored based on predefined rules. - - Args: - file_path: Path to check - - Returns: - bool: True if path should be ignored, False otherwise - """ - ignored_dirs: List[str] = ['.git'] - ignored_files_prefix: List[str] = ['repo-to-text_'] + ignored_dirs = ['.git'] + ignored_files_prefix = ['repo-to-text_'] is_ignored_dir = any(ignored in file_path for ignored in ignored_dirs) is_ignored_file = any(file_path.startswith(prefix) for prefix in ignored_files_prefix) result = is_ignored_dir or is_ignored_file if result: - logging.debug('Path ignored: %s', file_path) + logging.debug(f'Path ignored: {file_path}') return result - +def remove_empty_dirs(tree_output: str, path='.') -> str: + logging.debug('Removing empty directories from tree output') + lines = tree_output.splitlines() + non_empty_dirs = set() + filtered_lines = [] - -"""This module contains the CLI interface for the repo_to_text package.""" - -from .cli import create_default_settings_file, parse_args, main - -__all__ = ['create_default_settings_file', 'parse_args', 'main'] - - - - -""" -CLI for repo-to-text -""" - -import argparse -import textwrap -import os -import logging -import sys -from typing import NoReturn - -from ..utils.utils import setup_logging -from ..core.core import save_repo_to_text - -def create_default_settings_file() -> None: - """Create a default .repo-to-text-settings.yaml file.""" - settings_file = '.repo-to-text-settings.yaml' - if os.path.exists(settings_file): - raise FileExistsError( - f"The settings file '{settings_file}' already exists. " - "Please remove it or rename it if you want to create a new default settings file." - ) - - default_settings = textwrap.dedent("""\ - # Details: https://github.com/kirill-markin/repo-to-text - # Syntax: gitignore rules - - # Ignore files and directories for all sections from gitignore file - # Default: True - gitignore-import-and-ignore: True - - # Ignore files and directories for tree - # and contents sections (...) - ignore-tree-and-content: - - ".repo-to-text-settings.yaml" - - # Ignore files and directories for contents sections - ignore-content: - - "README.md" - - "LICENSE" - - "package-lock.json" - """) - with open('.repo-to-text-settings.yaml', 'w', encoding='utf-8') as f: - f.write(default_settings) - print("Default .repo-to-text-settings.yaml created.") - -def parse_args() -> argparse.Namespace: - """Parse command line arguments. + for line in lines: + parts = line.strip().split() + if parts: + full_path = parts[-1] + if os.path.isdir(full_path) and not any(os.path.isfile(os.path.join(full_path, f)) for f in os.listdir(full_path)): + logging.debug(f'Directory is empty and will be removed: {full_path}') + continue + non_empty_dirs.add(os.path.dirname(full_path)) + filtered_lines.append(line) - Returns: - argparse.Namespace: Parsed command line arguments - """ - parser = argparse.ArgumentParser( - description='Convert repository structure and contents to text' - ) - parser.add_argument('input_dir', nargs='?', default='.', help='Directory to process') + final_lines = [] + for line in filtered_lines: + parts = line.strip().split() + if parts: + full_path = parts[-1] + if os.path.isdir(full_path) and full_path not in non_empty_dirs: + logging.debug(f'Directory is empty and will be removed: {full_path}') + continue + final_lines.append(line) + + logging.debug('Empty directory removal complete') + return '\n'.join(final_lines) + +def save_repo_to_text(path='.', output_dir=None) -> str: + logging.debug(f'Starting to save repo structure to text for path: {path}') + gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) + tree_structure = get_tree_structure(path, gitignore_spec, tree_and_content_ignore_spec) + tree_structure = remove_empty_dirs(tree_structure, path) + + # Add timestamp to the output file name with a descriptive name + timestamp = datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-UTC') + output_file = f'repo-to-text_{timestamp}.txt' + + # Determine the full path to the output file + if output_dir: + if not os.path.exists(output_dir): + os.makedirs(output_dir) + output_file = os.path.join(output_dir, output_file) + + with open(output_file, 'w') as file: + project_name = os.path.basename(os.path.abspath(path)) + file.write(f'Directory: {project_name}\n\n') + file.write('Directory Structure:\n') + file.write('```\n.\n') + + # Insert .gitignore if it exists + if os.path.exists(os.path.join(path, '.gitignore')): + file.write('├── .gitignore\n') + + file.write(tree_structure + '\n' + '```\n') + logging.debug('Tree structure written to file') + + for root, _, files in os.walk(path): + for filename in files: + file_path = os.path.join(root, filename) + relative_path = os.path.relpath(file_path, path) + + if should_ignore_file(file_path, relative_path, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec): + continue + + relative_path = relative_path.replace('./', '', 1) + + file.write(f'\nContents of {relative_path}:\n') + file.write('```\n') + try: + with open(file_path, 'r', encoding='utf-8') as f: + file.write(f.read()) + except UnicodeDecodeError: + logging.debug(f'Could not decode file contents: {file_path}') + file.write('[Could not decode file contents]\n') + file.write('\n```\n') + + file.write('\n') + logging.debug('Repository contents written to file') + + # Read the contents of the generated file + with open(output_file, 'r') as file: + repo_text = file.read() + + # Copy the contents to the clipboard + pyperclip.copy(repo_text) + logging.debug('Repository structure and contents copied to clipboard') + + return output_file + +def main(): + parser = argparse.ArgumentParser(description='Convert repository structure and contents to text') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--output-dir', type=str, help='Directory to save the output file') - parser.add_argument( - '--create-settings', - '--init', - action='store_true', - help='Create default .repo-to-text-settings.yaml file' - ) - parser.add_argument('--stdout', action='store_true', help='Output to stdout instead of a file') - parser.add_argument( - '--ignore-patterns', - nargs='*', - help="List of files or directories to ignore in both tree and content sections. " - "Supports wildcards (e.g., '*')." - ) - return parser.parse_args() + args = parser.parse_args() -def main() -> NoReturn: - """Main entry point for the CLI. - - Raises: - SystemExit: Always exits with code 0 on success - """ - args = parse_args() setup_logging(debug=args.debug) logging.debug('repo-to-text script started') + save_repo_to_text(output_dir=args.output_dir) + logging.debug('repo-to-text script finished') - try: - if args.create_settings: - create_default_settings_file() - logging.debug('.repo-to-text-settings.yaml file created') - else: - save_repo_to_text( - path=args.input_dir, - output_dir=args.output_dir, - to_stdout=args.stdout, - cli_ignore_patterns=args.ignore_patterns - ) +if __name__ == '__main__': + main() - logging.debug('repo-to-text script finished') - sys.exit(0) - except (FileNotFoundError, FileExistsError, PermissionError, OSError) as e: - logging.error('Error occurred: %s', str(e)) - sys.exit(1) +``` - - - diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 92b8235..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1296 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. - -[[package]] -name = "argparse" -version = "1.4.0" -description = "Python command-line parsing library" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"}, - {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"}, -] - -[[package]] -name = "astroid" -version = "3.3.10" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, - {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[[package]] -name = "backports-tarfile" -version = "1.2.0" -description = "Backport of CPython tarfile module" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version <= \"3.11\"" -files = [ - {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, - {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] - -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "build" -version = "1.2.2.post1" -description = "A simple, correct Python build frontend" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, - {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} -packaging = ">=19.1" -pyproject_hooks = "*" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] -test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0) ; python_version < \"3.10\"", "setuptools (>=56.0.0) ; python_version == \"3.10\"", "setuptools (>=56.0.0) ; python_version == \"3.11\"", "setuptools (>=67.8.0) ; python_version >= \"3.12\"", "wheel (>=0.36.0)"] -typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] -uv = ["uv (>=0.1.18)"] -virtualenv = ["virtualenv (>=20.0.35)"] - -[[package]] -name = "certifi" -version = "2025.4.26" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, - {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\" or platform_system == \"Windows\" or os_name == \"nt\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "cryptography" -version = "43.0.3" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "dill" -version = "0.4.0" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "id" -version = "1.5.0" -description = "A tool for generating OIDC identities" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, - {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, -] - -[package.dependencies] -requests = "*" - -[package.extras] -dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] -lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] -test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version <= \"3.11\" or python_full_version < \"3.10.2\"" -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -description = "Utility functions for Python class constructs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, - {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, -] - -[package.dependencies] -more-itertools = "*" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "jaraco-context" -version = "6.0.1" -description = "Useful decorators and context managers" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, - {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, -] - -[package.dependencies] -"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] - -[[package]] -name = "jaraco-functools" -version = "4.1.0" -description = "Functools like those found in stdlib" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, - {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, -] - -[package.dependencies] -more-itertools = "*" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] -type = ["pytest-mypy"] - -[[package]] -name = "jeepney" -version = "0.9.0" -description = "Low-level, pure Python DBus protocol wrapper." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" -files = [ - {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, - {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, -] - -[package.extras] -test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["trio"] - -[[package]] -name = "keyring" -version = "25.6.0" -description = "Store and access your passwords safely." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, - {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, -] - -[package.dependencies] -importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} -"jaraco.classes" = "*" -"jaraco.context" = "*" -"jaraco.functools" = "*" -jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} -SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -completion = ["shtab (>=1.1.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] -type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "more-itertools" -version = "10.7.0" -description = "More routines for operating on iterables, beyond itertools" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"}, - {file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"}, -] - -[[package]] -name = "mypy" -version = "1.15.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, - {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, - {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, - {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, - {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, - {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, - {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, - {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, - {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, - {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, - {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, - {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, - {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, - {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nh3" -version = "0.2.21" -description = "Python binding to Ammonia HTML sanitizer Rust crate" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286"}, - {file = "nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde"}, - {file = "nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243"}, - {file = "nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b"}, - {file = "nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251"}, - {file = "nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b"}, - {file = "nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9"}, - {file = "nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d"}, - {file = "nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82"}, - {file = "nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585"}, - {file = "nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293"}, - {file = "nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431"}, - {file = "nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa"}, - {file = "nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1"}, - {file = "nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283"}, - {file = "nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a"}, - {file = "nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629"}, - {file = "nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pylint" -version = "3.3.7" -description = "python code static checker" -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, - {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, -] - -[package.dependencies] -astroid = ">=3.3.8,<=3.4.0.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version == \"3.11\""}, -] -isort = ">=4.2.5,<5.13 || >5.13,<7" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2" -tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -description = "Wrappers to call pyproject.toml-based build backend hooks." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, - {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, -] - -[[package]] -name = "pytest" -version = "8.3.5" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -description = "A (partial) reimplementation of pywin32 using ctypes/cffi" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"win32\"" -files = [ - {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, - {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "readme-renderer" -version = "44.0" -description = "readme_renderer is a library for rendering readme descriptions for Warehouse" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, - {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, -] - -[package.dependencies] -docutils = ">=0.21.2" -nh3 = ">=0.2.14" -Pygments = ">=2.5.1" - -[package.extras] -md = ["cmarkgfm (>=0.8.0)"] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "rfc3986" -version = "2.0.0" -description = "Validating URI References per RFC 3986" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, - {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, -] - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "rich" -version = "14.0.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "secretstorage" -version = "3.3.3" -description = "Python bindings to FreeDesktop.org Secret Service API" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" -files = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, -] - -[package.dependencies] -cryptography = ">=2.0" -jeepney = ">=0.6" - -[[package]] -name = "setuptools" -version = "75.3.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9"}, - {file = "setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] -core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "ruff (<=0.7.1)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.12.*)", "pytest-mypy"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - -[[package]] -name = "twine" -version = "6.1.0" -description = "Collection of utilities for publishing packages on PyPI" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"}, - {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"}, -] - -[package.dependencies] -id = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} -packaging = ">=24.0" -readme-renderer = ">=35.0" -requests = ">=2.20" -requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" -rfc3986 = ">=1.4.0" -rich = ">=12.0.0" -urllib3 = ">=1.26.0" - -[package.extras] -keyring = ["keyring (>=15.1)"] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, -] - -[[package]] -name = "urllib3" -version = "2.4.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, - {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version <= \"3.11\" or python_full_version < \"3.10.2\"" -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.9" -content-hash = "b54c3be79a5b6fac2038c490e571f97fd6d5bcdbbd78d141794c687c60866553" diff --git a/pyproject.toml b/pyproject.toml index ff44c8d..1a0e941 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "hatchling.build" [project] name = "repo-to-text" -version = "0.8.0" +version = "0.5.4" authors = [ { name = "Kirill Markin", email = "markinkirill@gmail.com" }, ] -description = "Convert a directory structure and its contents into a single text file, including the tree output and file contents in structured XML format. It may be useful to chat with LLM about your code." +description = "Convert a directory structure and its contents into a single text file, including the tree output and file contents in markdown code blocks. It may be useful to chat with LLM about your code." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.6" license = { text = "MIT" } classifiers = [ "Programming Language :: Python :: 3", @@ -48,7 +48,3 @@ dev = [ disable = [ "C0303", ] - - - - diff --git a/repo_to_text/cli/cli.py b/repo_to_text/cli/cli.py index 911dd1a..c3b3166 100644 --- a/repo_to_text/cli/cli.py +++ b/repo_to_text/cli/cli.py @@ -30,20 +30,14 @@ def create_default_settings_file() -> None: gitignore-import-and-ignore: True # Ignore files and directories for tree - # and contents sections (...) + # and "Contents of ..." sections ignore-tree-and-content: - ".repo-to-text-settings.yaml" - # Ignore files and directories for contents sections + # Ignore files and directories for "Contents of ..." section ignore-content: - "README.md" - "LICENSE" - - "package-lock.json" - - # Optional: Maximum number of words per output file before splitting. - # If not specified or null, no splitting based on word count will occur. - # Must be a positive integer if set. - # maximum_word_count_per_file: 10000 """) with open('.repo-to-text-settings.yaml', 'w', encoding='utf-8') as f: f.write(default_settings) diff --git a/repo_to_text/core/core.py b/repo_to_text/core/core.py index ccc9460..70bee20 100644 --- a/repo_to_text/core/core.py +++ b/repo_to_text/core/core.py @@ -4,12 +4,11 @@ Core functionality for repo-to-text import os import subprocess -import platform from typing import Tuple, Optional, List, Dict, Any, Set from datetime import datetime, timezone from importlib.machinery import ModuleSpec import logging -import yaml # type: ignore +import yaml import pathspec from pathspec import PathSpec @@ -37,20 +36,12 @@ def get_tree_structure( def run_tree_command(path: str) -> str: """Run the tree command and return its output.""" - if platform.system() == "Windows": - cmd = ["cmd", "/c", "tree", "/a", "/f", path] - else: - cmd = ["tree", "-a", "-f", "--noreport", path] - result = subprocess.run( - cmd, + ['tree', '-a', '-f', '--noreport', path], stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - encoding='utf-8', check=True ) - return result.stdout + return result.stdout.decode('utf-8') def filter_tree_output( tree_output: str, @@ -83,22 +74,7 @@ def process_line( if not full_path or full_path == '.': return None - try: - relative_path = os.path.relpath(full_path, path).replace(os.sep, '/') - except (ValueError, OSError) as e: - # Handle case where relpath fails (e.g., in CI when cwd is unavailable) - # Use absolute path conversion as fallback - logging.debug('os.path.relpath failed for %s, using fallback: %s', full_path, e) - if os.path.isabs(full_path) and os.path.isabs(path): - # Both are absolute, try manual relative calculation - try: - common = os.path.commonpath([full_path, path]) - relative_path = os.path.relpath(full_path, common).replace(os.sep, '/') - except (ValueError, OSError): - # Last resort: use just the filename - relative_path = os.path.basename(full_path) - else: - relative_path = os.path.basename(full_path) + relative_path = os.path.relpath(full_path, path).replace(os.sep, '/') if should_ignore_file( full_path, @@ -142,7 +118,7 @@ def load_ignore_specs( cli_ignore_patterns: List of patterns from command line Returns: - Tuple[Optional[PathSpec], Optional[PathSpec], PathSpec]: Tuple of gitignore_spec, + Tuple[Optional[PathSpec], Optional[PathSpec], PathSpec]: Tuple of gitignore_spec, content_ignore_spec, and tree_and_content_ignore_spec """ gitignore_spec = None @@ -152,21 +128,16 @@ def load_ignore_specs( repo_settings_path = os.path.join(path, '.repo-to-text-settings.yaml') if os.path.exists(repo_settings_path): - logging.debug( - 'Loading .repo-to-text-settings.yaml for ignore specs from path: %s', - repo_settings_path - ) + logging.debug('Loading .repo-to-text-settings.yaml from path: %s', repo_settings_path) with open(repo_settings_path, 'r', encoding='utf-8') as f: settings: Dict[str, Any] = yaml.safe_load(f) use_gitignore = settings.get('gitignore-import-and-ignore', True) if 'ignore-content' in settings: - content_ignore_spec = pathspec.PathSpec.from_lines( + content_ignore_spec: Optional[PathSpec] = pathspec.PathSpec.from_lines( 'gitwildmatch', settings['ignore-content'] ) if 'ignore-tree-and-content' in settings: - tree_and_content_ignore_list.extend( - settings.get('ignore-tree-and-content', []) - ) + tree_and_content_ignore_list.extend(settings.get('ignore-tree-and-content', [])) if cli_ignore_patterns: tree_and_content_ignore_list.extend(cli_ignore_patterns) @@ -183,30 +154,6 @@ def load_ignore_specs( ) return gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec -def load_additional_specs(path: str = '.') -> Dict[str, Any]: - """Load additional specifications from the settings file.""" - additional_specs: Dict[str, Any] = { - 'maximum_word_count_per_file': None - } - repo_settings_path = os.path.join(path, '.repo-to-text-settings.yaml') - if os.path.exists(repo_settings_path): - logging.debug( - 'Loading .repo-to-text-settings.yaml for additional specs from path: %s', - repo_settings_path - ) - with open(repo_settings_path, 'r', encoding='utf-8') as f: - settings: Dict[str, Any] = yaml.safe_load(f) - if 'maximum_word_count_per_file' in settings: - max_words = settings['maximum_word_count_per_file'] - if isinstance(max_words, int) and max_words > 0: - additional_specs['maximum_word_count_per_file'] = max_words - elif max_words is not None: # Allow null/None to mean "not set" - logging.warning( - "Invalid value for 'maximum_word_count_per_file': %s. " - "It must be a positive integer or null. Ignoring.", max_words - ) - return additional_specs - def should_ignore_file( file_path: str, relative_path: str, @@ -263,178 +210,57 @@ def save_repo_to_text( to_stdout: bool = False, cli_ignore_patterns: Optional[List[str]] = None ) -> str: - """Save repository structure and contents to a text file or multiple files.""" - # pylint: disable=too-many-locals + """Save repository structure and contents to a text file.""" logging.debug('Starting to save repo structure to text for path: %s', path) - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = ( - load_ignore_specs(path, cli_ignore_patterns) + gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs( + path, cli_ignore_patterns ) - additional_specs = load_additional_specs(path) - maximum_word_count_per_file = additional_specs.get( - 'maximum_word_count_per_file' - ) - tree_structure: str = get_tree_structure( path, gitignore_spec, tree_and_content_ignore_spec ) logging.debug('Final tree structure to be written: %s', tree_structure) - output_content_segments = generate_output_content( + output_content = generate_output_content( path, tree_structure, gitignore_spec, content_ignore_spec, - tree_and_content_ignore_spec, - maximum_word_count_per_file + tree_and_content_ignore_spec ) if to_stdout: - for segment in output_content_segments: - print(segment, end='') # Avoid double newlines if segments naturally end with one - # Return joined content for consistency, though primarily printed - return "".join(output_content_segments) + print(output_content) + return output_content - timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d-%H-%M-%S-UTC') - base_output_name_stem = f'repo-to-text_{timestamp}' - - output_filepaths: List[str] = [] + output_file = write_output_to_file(output_content, output_dir) + copy_to_clipboard(output_content) - if not output_content_segments: - logging.warning( - "generate_output_content returned no segments. No output file will be created." - ) - return "" # Or handle by creating an empty placeholder file - - if len(output_content_segments) == 1: - single_filename = f"{base_output_name_stem}.txt" - full_path_single_file = ( - os.path.join(output_dir, single_filename) if output_dir else single_filename - ) - - if output_dir and not os.path.exists(output_dir): - os.makedirs(output_dir) - - with open(full_path_single_file, 'w', encoding='utf-8') as f: - f.write(output_content_segments[0]) - output_filepaths.append(full_path_single_file) - copy_to_clipboard(output_content_segments[0]) - # Use basename for safe display in case relpath fails - display_path = os.path.basename(full_path_single_file) - print( - "[SUCCESS] Repository structure and contents successfully saved to " - f"file: \"{display_path}\"" - ) - else: # Multiple segments - if output_dir and not os.path.exists(output_dir): - os.makedirs(output_dir) # Create output_dir once if needed - - for i, segment_content in enumerate(output_content_segments): - part_filename = f"{base_output_name_stem}_part_{i+1}.txt" - full_path_part_file = ( - os.path.join(output_dir, part_filename) if output_dir else part_filename - ) - - with open(full_path_part_file, 'w', encoding='utf-8') as f: - f.write(segment_content) - output_filepaths.append(full_path_part_file) - - print( - f"[SUCCESS] Repository structure and contents successfully saved to " - f"{len(output_filepaths)} files:" - ) - for fp in output_filepaths: - # Use basename for safe display in case relpath fails - display_path = os.path.basename(fp) - print(f" - \"{display_path}\"") - - if output_filepaths: - # Return the actual file path for existence checks - return output_filepaths[0] - return "" - -def _read_file_content(file_path: str) -> str: - """Read file content, handling binary files and broken symlinks. - - Args: - file_path: Path to the file to read - - Returns: - str: File content or appropriate message for special cases - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - return f.read() - except UnicodeDecodeError: - logging.debug('Handling binary file contents: %s', file_path) - with open(file_path, 'rb') as f_bin: - binary_content: bytes = f_bin.read() - return binary_content.decode('latin1') - except FileNotFoundError as e: - # Minimal handling for bad symlinks - if os.path.islink(file_path) and not os.path.exists(file_path): - try: - target = os.readlink(file_path) - except OSError: - target = '' - return f"[symlink] -> {target}" - raise e + print( + "[SUCCESS] Repository structure and contents successfully saved to " + f"file: \"./{output_file}\"" + ) + return output_file def generate_output_content( path: str, tree_structure: str, gitignore_spec: Optional[PathSpec], content_ignore_spec: Optional[PathSpec], - tree_and_content_ignore_spec: Optional[PathSpec], - maximum_word_count_per_file: Optional[int] = None - ) -> List[str]: - """Generate the output content for the repository, potentially split into segments.""" - # pylint: disable=too-many-arguments - # pylint: disable=too-many-locals - # pylint: disable=too-many-positional-arguments - output_segments: List[str] = [] - current_segment_builder: List[str] = [] - current_segment_word_count: int = 0 + tree_and_content_ignore_spec: Optional[PathSpec] + ) -> str: + """Generate the output content for the repository.""" + output_content: List[str] = [] project_name = os.path.basename(os.path.abspath(path)) - - def count_words(text: str) -> int: - return len(text.split()) - - def _finalize_current_segment(): - nonlocal current_segment_word_count # Allow modification - if current_segment_builder: - output_segments.append("".join(current_segment_builder)) - current_segment_builder.clear() - current_segment_word_count = 0 - - def _add_chunk_to_output(chunk: str): - nonlocal current_segment_word_count - chunk_wc = count_words(chunk) - - if maximum_word_count_per_file is not None: - # If current segment is not empty, and adding this chunk would exceed limit, - # finalize the current segment before adding this new chunk. - if (current_segment_builder and - current_segment_word_count + chunk_wc > maximum_word_count_per_file): - _finalize_current_segment() - - current_segment_builder.append(chunk) - current_segment_word_count += chunk_wc - - # This logic ensures that if a single chunk itself is larger than the limit, - # it forms its own segment. The next call to _add_chunk_to_output - # or the final _finalize_current_segment will commit it. - - _add_chunk_to_output('\n') - _add_chunk_to_output(f'Directory: {project_name}\n\n') - _add_chunk_to_output('Directory Structure:\n') - _add_chunk_to_output('\n.\n') + output_content.append(f'Directory: {project_name}\n\n') + output_content.append('Directory Structure:\n') + output_content.append('```\n.\n') if os.path.exists(os.path.join(path, '.gitignore')): - _add_chunk_to_output('├── .gitignore\n') + output_content.append('├── .gitignore\n') - _add_chunk_to_output(tree_structure + '\n' + '\n') - logging.debug('Tree structure added to output content segment builder') + output_content.append(tree_structure + '\n' + '```\n') + logging.debug('Tree structure written to output content') for root, _, files in os.walk(path): for filename in files: @@ -450,48 +276,43 @@ def generate_output_content( ): continue - cleaned_relative_path = relative_path.replace('./', '', 1) - - _add_chunk_to_output(f'\n\n') - file_content = _read_file_content(file_path) - _add_chunk_to_output(file_content) - _add_chunk_to_output('\n\n') + relative_path = relative_path.replace('./', '', 1) - _add_chunk_to_output('\n\n') - - _finalize_current_segment() # Finalize any remaining content in the builder + output_content.append(f'\nContents of {relative_path}:\n') + output_content.append('```\n') + try: + with open(file_path, 'r', encoding='utf-8') as f: + output_content.append(f.read()) + except UnicodeDecodeError: + logging.debug('Could not decode file contents: %s', file_path) + output_content.append('[Could not decode file contents]\n') + output_content.append('\n```\n') - logging.debug( - 'Repository contents generated into %s segment(s)', len(output_segments) - ) - - # Ensure at least one segment is returned, even if it's just the empty repo structure - if not output_segments and not current_segment_builder: - # This case implies an empty repo and an extremely small word limit that split - # even the minimal tags. Or, if all content was filtered out. - # Return a minimal valid structure if everything else resulted in empty. - # However, the _add_chunk_to_output for repo tags should ensure - # current_segment_builder is not empty. And _finalize_current_segment ensures - # output_segments gets it. If output_segments is truly empty, it means an error - # or unexpected state. For safety, if it's empty, return a list with one empty - # string or minimal tags. Given the logic, this path is unlikely. - logging.warning( - "No output segments were generated. Returning a single empty segment." - ) - return ["\n\n"] + output_content.append('\n') + logging.debug('Repository contents written to output content') + return ''.join(output_content) - return output_segments +def write_output_to_file(output_content: str, output_dir: Optional[str]) -> str: + """Write the output content to a file.""" + timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d-%H-%M-%S-UTC') + output_file = f'repo-to-text_{timestamp}.txt' + if output_dir: + if not os.path.exists(output_dir): + os.makedirs(output_dir) + output_file = os.path.join(output_dir, output_file) -# The original write_output_to_file function is no longer needed as its logic -# is incorporated into save_repo_to_text for handling single/multiple files. + with open(output_file, 'w', encoding='utf-8') as file: + file.write(output_content) + + return output_file def copy_to_clipboard(output_content: str) -> None: """Copy the output content to the clipboard if possible.""" try: import importlib.util # pylint: disable=import-outside-toplevel - spec: Optional[ModuleSpec] = importlib.util.find_spec("pyperclip") # type: ignore + spec: Optional[ModuleSpec] = importlib.util.find_spec("pyperclip") if spec: import pyperclip # pylint: disable=import-outside-toplevel # type: ignore pyperclip.copy(output_content) # type: ignore diff --git a/tests/test_core.py b/tests/test_core.py index 1781502..1882388 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,20 +3,15 @@ import os import tempfile import shutil -from typing import Generator, IO +from typing import Generator import pytest -from unittest.mock import patch, mock_open, MagicMock -import yaml # For creating mock settings files easily - from repo_to_text.core.core import ( get_tree_structure, load_ignore_specs, should_ignore_file, is_ignored_path, - save_repo_to_text, - load_additional_specs, - generate_output_content + save_repo_to_text ) # pylint: disable=redefined-outer-name @@ -28,47 +23,6 @@ def temp_dir() -> Generator[str, None, None]: yield temp_path shutil.rmtree(temp_path) -# Mock tree outputs -# Raw output similar to `tree -a -f --noreport` -MOCK_RAW_TREE_FOR_SAMPLE_REPO = """./ -./.gitignore -./.repo-to-text-settings.yaml -./README.md -./src -./src/main.py -./tests -./tests/test_main.py -""" - -MOCK_RAW_TREE_SPECIAL_CHARS = """./ -./special chars -./special chars/file with spaces.txt -""" - -MOCK_RAW_TREE_EMPTY_FILTERING = """./ -./src -./src/main.py -./tests -./tests/test_main.py -""" -# Note: ./empty_dir is removed, assuming tree or filter_tree_output would handle it. -# This makes the test focus on the rest of the logic if tree output is as expected. - -# Expected output from get_tree_structure (filtered) -MOCK_GTS_OUTPUT_FOR_SAMPLE_REPO = """. -├── .gitignore -├── README.md -├── src -│ └── main.py -└── tests - └── test_main.py""" - -MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO = """. -├── file1.txt -├── file2.txt -└── subdir - └── file3.txt""" - @pytest.fixture def sample_repo(tmp_path: str) -> str: """Create a sample repository structure for testing.""" @@ -94,7 +48,6 @@ ignore-tree-and-content: - ".repo-to-text-settings.yaml" ignore-content: - "README.md" - - "package-lock.json" """ } @@ -106,26 +59,6 @@ ignore-content: return tmp_path_str -@pytest.fixture -def simple_word_count_repo(tmp_path: str) -> str: - """Create a simple repository for word count testing.""" - repo_path = str(tmp_path) - files_content = { - "file1.txt": "This is file one. It has eight words.", # 8 words - "file2.txt": "File two is here. This makes six words.", # 6 words - "subdir/file3.txt": "Another file in a subdirectory, with ten words exactly." # 10 words - } - for file_path, content in files_content.items(): - full_path = os.path.join(repo_path, file_path) - os.makedirs(os.path.dirname(full_path), exist_ok=True) - with open(full_path, "w", encoding="utf-8") as f: - f.write(content) - return repo_path - -def count_words_for_test(text: str) -> int: - """Helper to count words consistently with core logic for tests.""" - return len(text.split()) - def test_is_ignored_path() -> None: """Test the is_ignored_path function.""" assert is_ignored_path(".git/config") is True @@ -177,12 +110,9 @@ def test_should_ignore_file(sample_repo: str) -> None: tree_and_content_ignore_spec ) is False -@patch('repo_to_text.core.core.run_tree_command', return_value=MOCK_RAW_TREE_FOR_SAMPLE_REPO) -@patch('repo_to_text.core.core.check_tree_command', return_value=True) -def test_get_tree_structure(mock_check_tree: MagicMock, mock_run_tree: MagicMock, sample_repo: str) -> None: +def test_get_tree_structure(sample_repo: str) -> None: """Test tree structure generation.""" gitignore_spec, _, tree_and_content_ignore_spec = load_ignore_specs(sample_repo) - # The .repo-to-text-settings.yaml in sample_repo ignores itself from tree and content tree_output = get_tree_structure(sample_repo, gitignore_spec, tree_and_content_ignore_spec) # Basic structure checks @@ -191,11 +121,8 @@ def test_get_tree_structure(mock_check_tree: MagicMock, mock_run_tree: MagicMock assert "main.py" in tree_output assert "test_main.py" in tree_output assert ".git" not in tree_output - assert ".repo-to-text-settings.yaml" not in tree_output # Should be filtered by tree_and_content_ignore_spec -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SAMPLE_REPO) -@patch('repo_to_text.core.core.check_tree_command', return_value=True) # In case any internal call still checks -def test_save_repo_to_text(mock_check_tree: MagicMock, mock_get_tree: MagicMock, sample_repo: str) -> None: +def test_save_repo_to_text(sample_repo: str) -> None: """Test the main save_repo_to_text function.""" # Create output directory output_dir = os.path.join(sample_repo, "output") @@ -209,7 +136,7 @@ def test_save_repo_to_text(mock_check_tree: MagicMock, mock_get_tree: MagicMock, # Test file output output_file = save_repo_to_text(sample_repo, output_dir=output_dir) assert os.path.exists(output_file) - assert os.path.abspath(os.path.dirname(output_file)) == os.path.abspath(output_dir) + assert os.path.dirname(output_file) == output_dir # Check file contents with open(output_file, 'r', encoding='utf-8') as f: @@ -261,20 +188,15 @@ def test_load_ignore_specs_without_gitignore(temp_dir: str) -> None: assert content_ignore_spec is None assert tree_and_content_ignore_spec is not None -@patch('repo_to_text.core.core.run_tree_command', return_value=MOCK_RAW_TREE_SPECIAL_CHARS) -@patch('repo_to_text.core.core.check_tree_command', return_value=True) -def test_get_tree_structure_with_special_chars(mock_check_tree: MagicMock, mock_run_tree: MagicMock, temp_dir: str) -> None: +def test_get_tree_structure_with_special_chars(temp_dir: str) -> None: """Test tree structure generation with special characters in paths.""" # Create files with special characters - special_dir = os.path.join(temp_dir, "special chars") # Matches MOCK_RAW_TREE_SPECIAL_CHARS + special_dir = os.path.join(temp_dir, "special chars") os.makedirs(special_dir) with open(os.path.join(special_dir, "file with spaces.txt"), "w", encoding='utf-8') as f: f.write("test") - # load_ignore_specs will be called inside; for temp_dir, they will be None or empty. - gitignore_spec, _, tree_and_content_ignore_spec = load_ignore_specs(temp_dir) - tree_output = get_tree_structure(temp_dir, gitignore_spec, tree_and_content_ignore_spec) - + tree_output = get_tree_structure(temp_dir) assert "special chars" in tree_output assert "file with spaces.txt" in tree_output @@ -317,12 +239,10 @@ def test_save_repo_to_text_with_binary_files(temp_dir: str) -> None: # Check that the binary file is listed in the structure assert "binary.bin" in output # Check that the file content section exists with raw binary content - expected_content = f"\n{binary_content.decode('latin1')}\n" + expected_content = f"Contents of binary.bin:\n```\n{binary_content.decode('latin1')}\n```" assert expected_content in output -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) # Using simple repo tree for generic content -@patch('repo_to_text.core.core.check_tree_command', return_value=True) -def test_save_repo_to_text_custom_output_dir(mock_check_tree: MagicMock, mock_get_tree: MagicMock, temp_dir: str) -> None: +def test_save_repo_to_text_custom_output_dir(temp_dir: str) -> None: """Test save_repo_to_text with custom output directory.""" # Create a simple file structure with open(os.path.join(temp_dir, "test.txt"), "w", encoding='utf-8') as f: @@ -333,10 +253,8 @@ def test_save_repo_to_text_custom_output_dir(mock_check_tree: MagicMock, mock_ge output_file = save_repo_to_text(temp_dir, output_dir=output_dir) assert os.path.exists(output_file) - assert os.path.abspath(os.path.dirname(output_file)) == os.path.abspath(output_dir) - # output_file is relative, output_dir is absolute. This assertion needs care. - # Let's assert that the absolute path of output_file starts with absolute output_dir - assert os.path.abspath(output_file).startswith(os.path.abspath(output_dir)) + assert os.path.dirname(output_file) == output_dir + assert output_file.startswith(output_dir) def test_get_tree_structure_empty_directory(temp_dir: str) -> None: """Test tree structure generation for empty directory.""" @@ -344,9 +262,7 @@ def test_get_tree_structure_empty_directory(temp_dir: str) -> None: # Should only contain the directory itself assert tree_output.strip() == "" or tree_output.strip() == temp_dir -@patch('repo_to_text.core.core.run_tree_command', return_value=MOCK_RAW_TREE_EMPTY_FILTERING) -@patch('repo_to_text.core.core.check_tree_command', return_value=True) -def test_empty_dirs_filtering(mock_check_tree: MagicMock, mock_run_tree: MagicMock, tmp_path: str) -> None: +def test_empty_dirs_filtering(tmp_path: str) -> None: """Test filtering of empty directories in tree structure generation.""" # Create test directory structure with normalized paths base_path = os.path.normpath(tmp_path) @@ -385,364 +301,5 @@ def test_empty_dirs_filtering(mock_check_tree: MagicMock, mock_run_tree: MagicMo # Check that no line contains 'empty_dir' assert "empty_dir" not in line, f"Found empty_dir in line: {line}" -# Tests for maximum_word_count_per_file functionality - -def test_load_additional_specs_valid_max_words(tmp_path: str) -> None: - """Test load_additional_specs with a valid maximum_word_count_per_file.""" - settings_content = {"maximum_word_count_per_file": 1000} - settings_file = os.path.join(tmp_path, ".repo-to-text-settings.yaml") - with open(settings_file, "w", encoding="utf-8") as f: - yaml.dump(settings_content, f) - - specs = load_additional_specs(tmp_path) - assert specs["maximum_word_count_per_file"] == 1000 - -def test_load_additional_specs_invalid_max_words_string(tmp_path: str, caplog: pytest.LogCaptureFixture) -> None: - """Test load_additional_specs with an invalid string for maximum_word_count_per_file.""" - settings_content = {"maximum_word_count_per_file": "not-an-integer"} - settings_file = os.path.join(tmp_path, ".repo-to-text-settings.yaml") - with open(settings_file, "w", encoding="utf-8") as f: - yaml.dump(settings_content, f) - - specs = load_additional_specs(tmp_path) - assert specs["maximum_word_count_per_file"] is None - assert "Invalid value for 'maximum_word_count_per_file': not-an-integer" in caplog.text - -def test_load_additional_specs_invalid_max_words_negative(tmp_path: str, caplog: pytest.LogCaptureFixture) -> None: - """Test load_additional_specs with a negative integer for maximum_word_count_per_file.""" - settings_content = {"maximum_word_count_per_file": -100} - settings_file = os.path.join(tmp_path, ".repo-to-text-settings.yaml") - with open(settings_file, "w", encoding="utf-8") as f: - yaml.dump(settings_content, f) - - specs = load_additional_specs(tmp_path) - assert specs["maximum_word_count_per_file"] is None - assert "Invalid value for 'maximum_word_count_per_file': -100" in caplog.text - -def test_load_additional_specs_max_words_is_none_in_yaml(tmp_path: str, caplog: pytest.LogCaptureFixture) -> None: - """Test load_additional_specs when maximum_word_count_per_file is explicitly null in YAML.""" - settings_content = {"maximum_word_count_per_file": None} # In YAML, this is 'null' - settings_file = os.path.join(tmp_path, ".repo-to-text-settings.yaml") - with open(settings_file, "w", encoding="utf-8") as f: - yaml.dump(settings_content, f) - - specs = load_additional_specs(tmp_path) - assert specs["maximum_word_count_per_file"] is None - assert "Invalid value for 'maximum_word_count_per_file'" not in caplog.text - -def test_load_additional_specs_max_words_not_present(tmp_path: str) -> None: - """Test load_additional_specs when maximum_word_count_per_file is not present.""" - settings_content = {"other_setting": "value"} - settings_file = os.path.join(tmp_path, ".repo-to-text-settings.yaml") - with open(settings_file, "w", encoding="utf-8") as f: - yaml.dump(settings_content, f) - - specs = load_additional_specs(tmp_path) - assert specs["maximum_word_count_per_file"] is None - -def test_load_additional_specs_no_settings_file(tmp_path: str) -> None: - """Test load_additional_specs when no settings file exists.""" - specs = load_additional_specs(tmp_path) - assert specs["maximum_word_count_per_file"] is None - -# Tests for generate_output_content related to splitting -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) -def test_generate_output_content_no_splitting_max_words_not_set(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None: - """Test generate_output_content with no splitting when max_words is not set.""" - path = simple_word_count_repo - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) - # tree_structure is now effectively MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO due to the mock - - segments = generate_output_content( - path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, - maximum_word_count_per_file=None - ) - mock_get_tree.assert_not_called() # We are passing tree_structure directly - assert len(segments) == 1 - assert "file1.txt" in segments[0] - assert "This is file one." in segments[0] - -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) -def test_generate_output_content_no_splitting_content_less_than_limit(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None: - """Test generate_output_content with no splitting when content is less than max_words limit.""" - path = simple_word_count_repo - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) - - segments = generate_output_content( - path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, - maximum_word_count_per_file=500 # High limit - ) - mock_get_tree.assert_not_called() - assert len(segments) == 1 - assert "file1.txt" in segments[0] - -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) -def test_generate_output_content_splitting_occurs(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None: - """Test generate_output_content when splitting occurs due to max_words limit.""" - path = simple_word_count_repo - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) - max_words = 30 - segments = generate_output_content( - path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, - maximum_word_count_per_file=max_words - ) - mock_get_tree.assert_not_called() - assert len(segments) > 1 - total_content = "".join(segments) - assert "file1.txt" in total_content - assert "This is file one." in total_content - for i, segment in enumerate(segments): - segment_word_count = count_words_for_test(segment) - if i < len(segments) - 1: # For all but the last segment - # A segment can be larger than max_words if a single chunk (e.g. file content block) is larger - assert segment_word_count <= max_words or \ - (segment_word_count > max_words and count_words_for_test(segment.splitlines()[-2]) > max_words) - else: # Last segment can be smaller - assert segment_word_count > 0 - -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) -def test_generate_output_content_splitting_very_small_limit(mock_get_tree: MagicMock, simple_word_count_repo: str) -> None: - """Test generate_output_content with a very small max_words limit.""" - path = simple_word_count_repo - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(path) - max_words = 10 # Very small limit - segments = generate_output_content( - path, MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, - maximum_word_count_per_file=max_words - ) - mock_get_tree.assert_not_called() - assert len(segments) > 3 # Expect multiple splits due to small limit and multiple chunks - total_content = "".join(segments) - assert "file1.txt" in total_content # Check presence of file name in overall output - - raw_file1_content = "This is file one. It has eight words." # 8 words - # Based on actual debug output, the closing tag is just "" (1 word) - closing_tag_content = "" # 1 word - - # With max_words = 10: - # The splitting logic works per chunk, so raw_content (8 words) + closing_tag (1 word) = 9 words total - # should fit in one segment when they're placed together - - # Debug: Let's see what segments actually look like in CI - print(f"\nDEBUG: Generated {len(segments)} segments:") - for i, segment in enumerate(segments): - print(f"Segment {i+1} ({count_words_for_test(segment)} words):") - print(f"'{segment}'") - print("---") - - found_raw_content_segment = False - for segment in segments: - if raw_file1_content in segment: - # Check if this segment contains raw content with closing tag (total 9 words) - segment_wc = count_words_for_test(segment) - if closing_tag_content in segment: - # Raw content (8 words) + closing tag (1 word) = 9 words total - expected_word_count = count_words_for_test(raw_file1_content) + count_words_for_test(closing_tag_content) - assert segment_wc == expected_word_count # Should be 9 words - found_raw_content_segment = True - break - else: - # Segment contains opening tag + raw content (2 + 8 = 10 words) - # Opening tag: (2 words) - # Raw content: "This is file one. It has eight words." (8 words) - opening_tag_word_count = 2 # - expected_word_count = opening_tag_word_count + count_words_for_test(raw_file1_content) - assert segment_wc == expected_word_count # Should be 10 words - found_raw_content_segment = True - break - assert found_raw_content_segment, "Segment with raw file1 content not found or not matching expected structure" - -@patch('repo_to_text.core.core.get_tree_structure') # Will use a specific mock inside -def test_generate_output_content_file_header_content_together(mock_get_tree: MagicMock, tmp_path: str) -> None: - """Test that file header and its content are not split if word count allows.""" - repo_path = str(tmp_path) - file_content_str = "word " * 15 # 15 words - # Tags: \n (3) + \n (2) = 5 words. Total block = 20 words. - files_content = {"single_file.txt": file_content_str.strip()} - for file_path_key, content_val in files_content.items(): - full_path = os.path.join(repo_path, file_path_key) - os.makedirs(os.path.dirname(full_path), exist_ok=True) - with open(full_path, "w", encoding="utf-8") as f: - f.write(content_val) - - gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec = load_ignore_specs(repo_path) - # Mock the tree structure for this specific test case - mock_tree_for_single_file = ".\n└── single_file.txt" - mock_get_tree.return_value = mock_tree_for_single_file # This mock is for any internal calls if any - - max_words_sufficient = 35 # Enough for header + this one file block (around 20 words + initial header) - segments = generate_output_content( - repo_path, mock_tree_for_single_file, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, - maximum_word_count_per_file=max_words_sufficient - ) - assert len(segments) == 1 # Expect no splitting of this file from its tags - expected_file_block = f'\n{file_content_str.strip()}\n' - assert expected_file_block in segments[0] - - # Test if it splits if max_words is too small for the file block (20 words) - max_words_small = 10 - segments_small_limit = generate_output_content( - repo_path, mock_tree_for_single_file, gitignore_spec, content_ignore_spec, tree_and_content_ignore_spec, - maximum_word_count_per_file=max_words_small - ) - # The file block (20 words) is a single chunk. It will form its own segment. - # Header part will be one segment. File block another. Footer another. - assert len(segments_small_limit) >= 2 - - found_raw_content_in_own_segment = False - raw_content_single_file = "word " * 15 # 15 words - # expected_file_block is the whole thing (20 words) - # With max_words_small = 10: - # 1. Opening tag (3 words) -> new segment - # 2. Raw content (15 words) -> new segment (because 0 + 15 > 10) - # 3. Closing tag (2 words) -> new segment (because 0 + 2 <= 10, but follows a large chunk) - - for segment in segments_small_limit: - if raw_content_single_file.strip() in segment.strip() and \ - '' not in segment and \ - '' not in segment: - # This segment should contain only the raw 15 words - assert count_words_for_test(segment.strip()) == 15 - found_raw_content_in_own_segment = True - break - assert found_raw_content_in_own_segment, "Raw content of single_file.txt not found in its own segment" - -# Tests for save_repo_to_text related to splitting -@patch('repo_to_text.core.core.load_additional_specs') -@patch('repo_to_text.core.core.generate_output_content') -@patch('repo_to_text.core.core.os.makedirs') -@patch('builtins.open', new_callable=mock_open) -@patch('repo_to_text.core.core.copy_to_clipboard') -def test_save_repo_to_text_no_splitting_mocked( - mock_copy_to_clipboard: MagicMock, - mock_file_open: MagicMock, - mock_makedirs: MagicMock, - mock_generate_output: MagicMock, - mock_load_specs: MagicMock, - simple_word_count_repo: str, - tmp_path: str -) -> None: - """Test save_repo_to_text: no splitting, single file output.""" - mock_load_specs.return_value = {'maximum_word_count_per_file': None} - mock_generate_output.return_value = ["Single combined content\nfile1.txt\ncontent1"] - output_dir = os.path.join(str(tmp_path), "output") - - with patch('repo_to_text.core.core.datetime') as mock_datetime: - mock_datetime.now.return_value.strftime.return_value = "mock_timestamp" - returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir) - - mock_load_specs.assert_called_once_with(simple_word_count_repo) - mock_generate_output.assert_called_once() - expected_filename = os.path.join(output_dir, "repo-to-text_mock_timestamp.txt") - assert os.path.basename(returned_path) == os.path.basename(expected_filename) - mock_makedirs.assert_called_once_with(output_dir) - mock_file_open.assert_called_once_with(expected_filename, 'w', encoding='utf-8') - mock_file_open().write.assert_called_once_with("Single combined content\nfile1.txt\ncontent1") - mock_copy_to_clipboard.assert_called_once_with("Single combined content\nfile1.txt\ncontent1") - -@patch('repo_to_text.core.core.load_additional_specs') -@patch('repo_to_text.core.core.generate_output_content') -@patch('repo_to_text.core.core.os.makedirs') -@patch('builtins.open') -@patch('repo_to_text.core.core.copy_to_clipboard') -def test_save_repo_to_text_splitting_occurs_mocked( - mock_copy_to_clipboard: MagicMock, - mock_open_function: MagicMock, - mock_makedirs: MagicMock, - mock_generate_output: MagicMock, - mock_load_specs: MagicMock, - simple_word_count_repo: str, - tmp_path: str -) -> None: - """Test save_repo_to_text: splitting occurs, multiple file outputs with better write check.""" - mock_load_specs.return_value = {'maximum_word_count_per_file': 50} - segments_content = ["Segment 1 content data", "Segment 2 content data"] - mock_generate_output.return_value = segments_content - output_dir = os.path.join(str(tmp_path), "output_split_adv") - - mock_file_handle1 = MagicMock(spec=IO) - mock_file_handle2 = MagicMock(spec=IO) - mock_open_function.side_effect = [mock_file_handle1, mock_file_handle2] - - with patch('repo_to_text.core.core.datetime') as mock_datetime: - mock_datetime.now.return_value.strftime.return_value = "mock_ts_split_adv" - returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir) - - expected_filename_part1 = os.path.join(output_dir, "repo-to-text_mock_ts_split_adv_part_1.txt") - expected_filename_part2 = os.path.join(output_dir, "repo-to-text_mock_ts_split_adv_part_2.txt") - - assert os.path.basename(returned_path) == os.path.basename(expected_filename_part1) - mock_makedirs.assert_called_once_with(output_dir) - - mock_open_function.assert_any_call(expected_filename_part1, 'w', encoding='utf-8') - mock_open_function.assert_any_call(expected_filename_part2, 'w', encoding='utf-8') - assert mock_open_function.call_count == 2 - - mock_file_handle1.__enter__().write.assert_called_once_with(segments_content[0]) - mock_file_handle2.__enter__().write.assert_called_once_with(segments_content[1]) - - mock_copy_to_clipboard.assert_not_called() - -@patch('repo_to_text.core.core.copy_to_clipboard') -@patch('builtins.open', new_callable=mock_open) -@patch('repo_to_text.core.core.os.makedirs') -@patch('repo_to_text.core.core.generate_output_content') # This is the one that will be used -@patch('repo_to_text.core.core.load_additional_specs') # This is the one that will be used -@patch('repo_to_text.core.core.get_tree_structure', return_value=MOCK_GTS_OUTPUT_FOR_SIMPLE_REPO) -def test_save_repo_to_text_stdout_with_splitting( - mock_get_tree: MagicMock, # Order of mock args should match decorator order (bottom-up) - mock_load_specs: MagicMock, - mock_generate_output: MagicMock, - mock_os_makedirs: MagicMock, - mock_file_open: MagicMock, - mock_copy_to_clipboard: MagicMock, - simple_word_count_repo: str, - capsys: pytest.CaptureFixture[str] -) -> None: - """Test save_repo_to_text with to_stdout=True and content that would split.""" - mock_load_specs.return_value = {'maximum_word_count_per_file': 10} - mock_generate_output.return_value = ["Segment 1 for stdout.", "Segment 2 for stdout."] - - result_string = save_repo_to_text(simple_word_count_repo, to_stdout=True) - - mock_load_specs.assert_called_once_with(simple_word_count_repo) - mock_get_tree.assert_called_once() # Assert that get_tree_structure was called - mock_generate_output.assert_called_once() - mock_os_makedirs.assert_not_called() - mock_file_open.assert_not_called() - mock_copy_to_clipboard.assert_not_called() - - captured = capsys.readouterr() - assert "Segment 1 for stdout.Segment 2 for stdout." == captured.out.strip() # Added strip() to handle potential newlines from logging - assert result_string == "Segment 1 for stdout.Segment 2 for stdout." - -@patch('repo_to_text.core.core.load_additional_specs') -@patch('repo_to_text.core.core.generate_output_content') -@patch('repo_to_text.core.core.os.makedirs') -@patch('builtins.open', new_callable=mock_open) -@patch('repo_to_text.core.core.copy_to_clipboard') -def test_save_repo_to_text_empty_segments( - mock_copy_to_clipboard: MagicMock, - mock_file_open: MagicMock, - mock_makedirs: MagicMock, - mock_generate_output: MagicMock, - mock_load_specs: MagicMock, - simple_word_count_repo: str, - tmp_path: str, - caplog: pytest.LogCaptureFixture -) -> None: - """Test save_repo_to_text when generate_output_content returns no segments.""" - mock_load_specs.return_value = {'maximum_word_count_per_file': None} - mock_generate_output.return_value = [] - output_dir = os.path.join(str(tmp_path), "output_empty") - - returned_path = save_repo_to_text(simple_word_count_repo, output_dir=output_dir) - - assert returned_path == "" - mock_makedirs.assert_not_called() - mock_file_open.assert_not_called() - mock_copy_to_clipboard.assert_not_called() - assert "generate_output_content returned no segments" in caplog.text - if __name__ == "__main__": pytest.main([__file__])