diff --git a/.code2promptrc b/.code2promptrc new file mode 100644 index 0000000..604daf0 --- /dev/null +++ b/.code2promptrc @@ -0,0 +1,8 @@ +{ + "suppress_comments": false, + "line_number": false, + "log_level": "INFO", + "encoding": "cl100k_base", + "filter": "*.py,*.js", + "exclude": "tests/*,docs/*" +} diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..c387bff --- /dev/null +++ b/.cursorrules @@ -0,0 +1,31 @@ + +# Role Overview + +You are an elite software developer with extensive expertise in Python, command-line tools, and file system operations. Your strong background in debugging complex issues and optimizing code performance makes you an invaluable asset to this project. + +## Key Attributes + +- **Pragmatic Approach**: You prioritize delivering high-quality, maintainable code that meets project requirements. +- **Modular Design**: You embrace composability and modularity, ensuring that your code is easy to extend and maintain. +- **Principled Coding**: You adhere to the KISS (Keep It Simple, Stupid) and DRY (Don't Repeat Yourself) principles, promoting simplicity and efficiency. +- **Documentation & Testing**: You recognize the importance of clear documentation and thorough testing to guarantee the reliability of your work. +- **Functional Preference**: You prefer using functions and modules over classes, focusing on functional programming paradigms. + +## Technological Stack + +This project utilizes the following technologies: + +- **Python Version**: 3.6+ +- **Dependencies**: + - `python = "^3.8,<4.0"` + - `rich = "^13.7.1"` # For rich text and beautiful formatting + - `click = "^8.1.7"` # For creating elegant command-line interfaces + - `jinja2 = "^3.1.4"` # For template rendering + - `prompt-toolkit = "^3.0.47"` # For building powerful interactive command-line applications + - `tiktoken = "^0.7.0"` # For tokenization tasks + - `pyperclip = "^1.9.0"` # For clipboard operations + - `colorama = "^0.4.6"` # For colored terminal text output + - `tqdm = "^4.66.4"` # For progress bars + - `tabulate = "^0.9.0"` # For tabular data formatting + - `pydantic` # For data validation and type checking + - `poetry` # For dependency management diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 163c6f2..d434d7a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ dist __pycache__ .pytest_cache *.pyc -.DS_Store \ No newline at end of file +.DS_Store +.tasks +cli.log +.aider* +.ruff_cache \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1cd0f6e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +## [Unreleased] + +### Added + +- New features and improvements planned for the next release. + +### Changed + +- Updates and enhancements under consideration for future versions. + +### Deprecated + +- Features that will be removed in future releases. + +### Removed + +- Features that have been removed in this version. + +### Fixed + +- Bug fixes that are currently being addressed. + +## [Released] + +### [0.8.1] - 2024-09-08 + +- **Fixed**: Fix the selection on individual files. You must install it. + +### [0.8.0] - 2024-09-07 + +- **Added**: `--interactive` mode for enhanced user interaction. + +### [0.6.12] - 2024-08-01 + +- **Fixed**: Bugs related to input variables and inclusion functionality. + +### [0.6.11] - 2024-07-15 + +- **Added**: Support for dynamic variables such as `{{input:var1}}` in templates. +- **Added**: Support for `{% include "./file1.txt" %}` feature. +- **Fixed**: Ensured variables are only processed once. +- **Updated**: Improved the price table for better clarity. + +### [0.6.9] - 2024-06-10 + +- **Improved**: Display of tokens for better user experience. + +### [0.6.8] - 2024-05-20 + +- **Improved**: `--create-templates-flags` functionality. +- **Enhanced**: Overall quality of templates. + +### [0.6.6] - 2024-04-15 + +- **Added**: New templates to expand functionality. +- **Fixed**: Bugs related to `--exclude` and `include` features. + +## Guidelines for Maintaining the Changelog + +- **Keep It Human-Friendly**: Ensure that the changelog is written for users, not just for developers. Avoid technical jargon where possible. + +- **Chronological Order**: List changes in reverse chronological order, with the most recent changes at the top. + +- **Linkable Versions**: Ensure that each version entry is linkable for easy reference. + +- **Clear Sections**: Use distinct sections for added, changed, deprecated, removed, and fixed items to enhance readability. + +- **Release Dates**: Include the release date for each version to provide context for users. + +- **Unreleased Section**: Maintain an "Unreleased" section at the top for tracking upcoming changes, which helps users anticipate future updates. + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fb798f3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others’ private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at contact@elitizon.com. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c662e6b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,104 @@ +# Contributing to code2prompt + +Thank you for your interest in contributing to code2prompt! We welcome contributions from the community to help improve and grow this project. This document outlines the process for contributing and provides guidelines to ensure a smooth collaboration. + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Development Setup](#development-setup) +3. [Making Changes](#making-changes) +4. [Submitting Changes](#submitting-changes) +5. [Coding Standards](#coding-standards) +6. [Running Tests](#running-tests) +7. [Reporting Issues](#reporting-issues) +8. [Community Guidelines](#community-guidelines) + +## Getting Started + +Before you begin: + +1. Ensure you have a [GitHub account](https://github.com/signup). +2. Familiarize yourself with the [code2prompt documentation](https://github.com/raphaelmansuy/code2prompt#readme). +3. Check the [issues page](https://github.com/raphaelmansuy/code2prompt/issues) for existing issues or feature requests. + +## Development Setup + +To set up your development environment: + +1. Fork the repository on GitHub. +2. Clone your fork locally: + ``` + git clone https://github.com/your-username/code2prompt.git + cd code2prompt + ``` +3. Ensure you have Python 3.7+ and [Poetry](https://python-poetry.org/docs/#installation) installed. +4. Install dependencies using Poetry: + ``` + poetry install + ``` +5. Activate the virtual environment: + ``` + poetry shell + ``` + +## Making Changes + +1. Create a new branch for your changes: + ``` + git checkout -b feature/your-feature-name + ``` +2. Make your changes and commit them with a clear, descriptive commit message. +3. Add or update tests as necessary. +4. Update documentation if you're changing functionality. + +## Submitting Changes + +1. Push your changes to your fork: + ``` + git push origin feature/your-feature-name + ``` +2. Submit a pull request to the main repository. +3. Ensure your PR description clearly describes the problem and solution. +4. Link any relevant issues in the PR description. + +## Coding Standards + +Please adhere to the following coding standards: + +1. Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code. +2. Use meaningful variable and function names. +3. Write clear, concise comments and docstrings. +4. Ensure your code is compatible with Python 3.7+. +5. Use type hints where appropriate. + +## Running Tests + +Before submitting your changes, make sure all tests pass: + +``` +poetry run pytest +``` + +If you've added new functionality, please include appropriate tests. + +## Reporting Issues + +When reporting issues: + +1. Use the [issue tracker](https://github.com/raphaelmansuy/code2prompt/issues). +2. Provide a clear, concise description of the issue. +3. Include steps to reproduce the problem. +4. Specify your operating system, Python version, and code2prompt version. +5. If possible, provide a minimal code example that demonstrates the issue. + +## Community Guidelines + +To ensure a positive and inclusive community: + +1. Be respectful and considerate in your interactions. +2. Provide constructive feedback. +3. Avoid offensive or discriminatory language. +4. Help others when you can. +5. Follow the [Code of Conduct](CODE_OF_CONDUCT.md). + +Thank you for contributing to code2prompt! Your efforts help make this project better for everyone. diff --git a/README.md b/README.md index 4d881f1..caf06fa 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,623 @@ - # Code2Prompt -Code2Prompt is a powerful command-line tool that simplifies the process of providing context to Large Language Models (LLMs) by generating a comprehensive Markdown file containing the content of your codebase. +[![PyPI version](https://badge.fury.io/py/code2prompt.svg)](https://badge.fury.io/py/code2prompt) + +[![GitHub Stars](https://img.shields.io/github/stars/raphaelmansuy/code2prompt.svg)](https://github.com/raphaelmansuy/code2prompt/stargazers) +[![GitHub Forks](https://img.shields.io/github/forks/raphaelmansuy/code2prompt.svg)](https://github.com/raphaelmansuy/code2prompt/network/members) +[![PyPI downloads](https://img.shields.io/pypi/dm/qllm.svg)](https://pypi.org/project/code2prompt/) +[![PyPI version](https://img.shields.io/pypi/v/qllm.svg)](https://pypi.org/project/code2prompt/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +Code2Prompt is a powerful command-line tool that generates comprehensive prompts from codebases, designed to streamline interactions between developers and Large Language Models (LLMs) for code analysis, documentation, and improvement tasks. + +## Table of Contents + +1. [Why Code2Prompt?](#why-code2prompt) +2. [Features](#features) +3. [Installation](#installation) +4. [Getting Started](#getting-started) +5. [Quick Start](#quick-start) +6. [Usage](#usage) +7. [Options](#options) +8. [Examples](#examples) +9. [Templating System](#templating-system) +10. [Integration with LLM CLI](#integration-with-llm-cli) +11. [GitHub Actions Integration](#github-actions-integration) +12. [Configuration File](#configuration-file) +13. [Troubleshooting](#troubleshooting) +14. [Contributing](#contributing) +15. [License](#license) + +# Code2Prompt: Transform Your Codebase into AI-Ready Prompts -With Code2Prompt, you can easily create a well-structured and informative document that serves as a valuable resource for feeding questions to LLMs, enabling them to better understand and assist with your code-related queries. +[![PyPI version](https://badge.fury.io/py/code2prompt.svg)](https://badge.fury.io/py/code2prompt) +[![GitHub Stars](https://img.shields.io/github/stars/raphaelmansuy/code2prompt.svg)](https://github.com/raphaelmansuy/code2prompt/stargazers) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -![Illustration](./docs/code2Prompt.jpg) +![](./docs/screen-example1.png) -## Features +## Supercharge Your AI-Assisted Development -- Automatically traverses a directory and its subdirectories to include all relevant files -- Supports filtering files based on patterns (e.g., "*.py" to include only Python files) -- Respects .gitignore files to exclude unwanted files and directories -- Generates a table of contents with links to each file section -- Provides file metadata such as extension, size, creation time, and modification time -- Optionally strips comments from code files to focus on the core code -- Includes the actual code content of each file in fenced code blocks -- Handles binary files and files with encoding issues gracefully +Code2Prompt is a powerful, open-source command-line tool that bridges the gap between your codebase and Large Language Models (LLMs). By converting your entire project into a comprehensive, AI-friendly prompt, Code2Prompt enables you to leverage the full potential of AI for code analysis, documentation, and improvement tasks. -## How It Works +### πŸš€ Key Features -The following diagram illustrates the high-level workflow of Code2Prompt: +- **Holistic Codebase Representation**: Generate a well-structured Markdown prompt that captures your entire project's essence, making it easier for LLMs to understand the context. +- **Intelligent Source Tree Generation**: Create a clear, hierarchical view of your codebase structure, allowing for better navigation and understanding of the project. +- **Customizable Prompt Templates**: Tailor your output using Jinja2 templates to suit specific AI tasks, enhancing the relevance of generated prompts. +- **Smart Token Management**: Count and optimize tokens to ensure compatibility with various LLM token limits, preventing errors during processing. +- **Gitignore Integration**: Respect your project's .gitignore rules for accurate representation, ensuring that irrelevant files are excluded from processing. +- **Flexible File Handling**: Filter and exclude files using powerful glob patterns, giving you control over which files are included in the prompt generation. +- **Custom Syntax Highlighting**: Pair custom file extensions with specific syntax highlighting using the `--syntax-map` option. For example, you can specify that `.inc` files should be treated as `bash` scripts. +- **Clipboard Ready**: Instantly copy generated prompts to your clipboard for quick AI interactions, streamlining your workflow. +- **Multiple Output Options**: Save to file or display in the console, providing flexibility in how you want to use the generated prompts. +- **Enhanced Code Readability**: Add line numbers to source code blocks for precise referencing, making it easier to discuss specific parts of the code. +- **Include file**: Support of template import, allowing for modular template design. +- **Input variables**: Support of Input Variables in templates, enabling dynamic prompt generation based on user input. -![Diagram](./docs/code2prompt.process.excalidraw.png) +### πŸ’‘ Why Code2Prompt? +- **Contextual Understanding**: Provide LLMs with a comprehensive view of your project for more accurate suggestions and analysis. +- **Consistency Boost**: Maintain coding style and conventions across your entire project, improving code quality. +- **Efficient Refactoring**: Enable better interdependency analysis and smarter refactoring recommendations, saving time and effort. +- **Improved Documentation**: Generate contextually relevant documentation that truly reflects your codebase, enhancing maintainability. +- **Pattern Recognition**: Help LLMs learn and apply your project-specific patterns and idioms, improving the quality of AI interactions. -1. The tool starts by parsing the command-line options provided by the user. -2. It then parses the .gitignore file (if specified) to obtain a set of patterns for excluding files and directories. -3. The tool traverses the specified directory and its subdirectories, processing each file encountered. -4. For each file, it checks if the file is ignored based on the .gitignore patterns. If ignored, it skips the file and moves to the next one. -5. If the file is not ignored, it checks if the file matches the filter pattern (if provided). If the file doesn't match the filter, it skips the file and moves to the next one. -6. If the file matches the filter pattern, it checks if the file is a binary file. If it is, it skips the file and moves to the next one. -7. If the file is not a binary file, the tool extracts the file metadata (extension, size, creation time, modification time). -8. It then reads the file content and generates a file summary and code block. -9. The file summary, code block, and metadata are appended to the Markdown content. -10. Steps 4-9 are repeated for each file in the directory and its subdirectories. -11. After processing all files, the tool generates a table of contents based on the file paths. -12. If an output file is specified, the generated Markdown content is written to the file. Otherwise, it is printed to the console. -13. The tool ends its execution. +Transform the way you interact with AI for software development. With Code2Prompt, harness the full power of your codebase in every AI conversation. + +Ready to elevate your AI-assisted development? Let's dive in! πŸŠβ€β™‚οΈ ## Installation -There are two ways to install Code2Prompt: +Choose one of the following methods to install Code2Prompt: + +### Using pip + +```bash +pip install code2prompt +``` +### Using [pipx](https://github.com/pypa/pipx) (recommended) + +```bash +pipx install code2prompt +``` + +## Getting Started + +To get started with Code2Prompt, follow these steps: -### Using Poetry +1. **Install Code2Prompt**: Use one of the installation methods mentioned above. +2. **Prepare Your Codebase**: Ensure your project is organized and that you have a `.gitignore` file if necessary. +3. **Run Code2Prompt**: Use the command line to generate prompts from your codebase. + +For example, to generate a prompt from a single Python file, run: + +```bash +code2prompt --path /path/to/your/script.py +``` -Code2Prompt is built using Poetry, a dependency management and packaging tool for Python. To install Code2Prompt using Poetry, follow these steps: +## Quick Start -1. Make sure you have Poetry installed. If you don't have it installed, you can install it by running: +1. Generate a prompt from a single Python file: + ```bash + code2prompt --path /path/to/your/script.py ``` - curl -sSL https://install.python-poetry.org | python3 - + +2. Process an entire project directory and save the output: + ```bash + code2prompt --path /path/to/your/project --output project_summary.md ``` -2. Clone the Code2Prompt repository: +3. Generate a prompt for multiple files, excluding tests: + ```bash + code2prompt --path /path/to/src --path /path/to/lib --exclude "*/tests/*" --output codebase_summary.md ``` - git clone https://github.com/raphael.mansuy/code2prompt.git + +## Usage + +The basic syntax for Code2Prompt is: + +```bash +code2prompt --path /path/to/your/code [OPTIONS] +``` + +For multiple paths: + +```bash +code2prompt --path /path/to/dir1 --path /path/to/file2.py [OPTIONS] +``` + +### Custom Syntax Highlighting + +To pair custom file extensions with specific syntax highlighting, use the `--syntax-map` option. This allows you to specify mappings in the format `extension:syntax`. For example: + +``` +code2prompt --path /path/to/your/code --syntax-map "inc:bash,customext:python,ext2:javascript" +``` + +This command will treat `.inc` files as `bash` scripts, `.customext` files as `python`, and `.ext2` files as `javascript`. + +You can also use multiple `--syntax-map` arguments or separate mappings with commas: + +``` +code2prompt --path /path/to/your/script.py --syntax-map "inc:bash" +``` + +``` +code2prompt --path /path/to/your/project --syntax-map "inc:bash,txt:markdown" --output project_summary.md +``` + +``` +code2prompt --path /path/to/src --path /path/to/lib --syntax-map "inc:bash,customext:python" --output codebase_summary.md +``` + +## Options + +| Option | Short | Description | +|--------|-------|-------------| +| `--path` | `-p` | Path(s) to the directory or file to process (required, multiple allowed) | +| `--output` | `-o` | Name of the output Markdown file | +| `--gitignore` | `-g` | Path to the .gitignore file | +| `--filter` | `-f` | Comma-separated filter patterns to include files (e.g., "*.py,*.js") | +| `--exclude` | `-e` | Comma-separated patterns to exclude files (e.g., "*.txt,*.md") | +| `--case-sensitive` | | Perform case-sensitive pattern matching | +| `--suppress-comments` | `-s` | Strip comments from the code files | +| `--line-number` | `-ln` | Add line numbers to source code blocks | +| `--no-codeblock` | | Disable wrapping code inside markdown code blocks | +| `--template` | `-t` | Path to a Jinja2 template file for custom prompt generation | +| `--tokens` | | Display the token count of the generated prompt | +| `--encoding` | | Specify the tokenizer encoding to use (default: "cl100k_base") | +| `--create-templates` | | Create a templates directory with example templates | +| `--version` | `-v` | Show the version and exit | +| `--log-level` | | Set the logging level (e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL) | +| `--interactive` | `-i` | Activate interactive mode for file selection | +| `--syntax-map` | | Pair custom file extensions with specific syntax highlighting (e.g., "inc:bash,customext:python,ext2:javascript") | + +## Command Parameters + +### `--filter` or `-f` and `--exclude` or `-e` + +The `--filter` and `--exclude` options allow you to specify patterns for files or directories that should be included in or excluded from processing, respectively. + +#### Syntax: +``` +--filter "PATTERN1,PATTERN2,..." +--exclude "PATTERN1,PATTERN2,..." +``` +or +``` +-f "PATTERN1,PATTERN2,..." +-e "PATTERN1,PATTERN2,..." +``` + +#### Description: +- Both options accept a comma-separated list of patterns. +- Patterns can include wildcards (`*`) and directory indicators (`**`). +- Case-sensitive by default (use `--case-sensitive` flag to change this behavior). +- `--exclude` patterns take precedence over `--filter` patterns. + +#### Examples: + +1. Include only Python files: + ``` + --filter "**.py" ``` -3. Navigate to the project directory: +2. Exclude all Markdown files: ``` - cd code2prompt + --exclude "**.md" ``` -4. Install the dependencies using Poetry: +3. Include specific file types in the src directory: ``` - poetry install + --filter "src/**.{js,ts}" ``` -### Using pipx +4. Exclude multiple file types and a specific directory: + ``` + --exclude "**.log,**.tmp,**/node_modules/**" + ``` -Alternatively, you can install Code2Prompt using pipx, a tool for installing and running Python applications in isolated environments. To install Code2Prompt using pipx, follow these steps: +5. Include all files except those in 'test' directories: + ``` + --filter "**" --exclude "**/test/**" + ``` -1. Make sure you have pipx installed. If you don't have it installed, you can install it by running: +6. Complex filtering (include JavaScript files, exclude minified and test files): ``` - python3 -m pip install --user pipx - python3 -m pipx ensurepath + --filter "**.js" --exclude "**.min.js,**test**.js" ``` -2. Install Code2Prompt using pipx: +7. Include specific files across all directories: ``` - pipx install git+https://github.com/raphael.mansuy/code2prompt.git + --filter "**/config.json,**/README.md" ``` - This command will clone the Code2Prompt repository and install it in an isolated environment managed by pipx. +8. Exclude temporary files and directories: + ``` + --exclude "**/.cache/**,**/tmp/**,**.tmp" + ``` -3. After installation, you can run Code2Prompt using the `code2prompt` command: +9. Include source files but exclude build output: ``` - code2prompt --path /path/to/your/codebase --output output.md + --filter "src/**/*.{js,ts}" --exclude "**/dist/**,**/build/**" ``` -Using pipx provides a convenient way to install and run Code2Prompt without affecting your system-wide Python installation. +10. Exclude version control and IDE-specific files: + ``` + --exclude "**/.git/**,**/.vscode/**,**/.idea/**" + ``` -## Usage +#### Important Notes: -To generate a Markdown file with the content of your codebase, use the following command: +- Always use double quotes around patterns to prevent shell interpretation of special characters. +- Patterns are matched against the full path of each file, relative to the project root. +- The `**` wildcard matches any number of directories. +- Single `*` matches any characters within a single directory or filename. +- Use commas to separate multiple patterns within the same option. +- Combine `--filter` and `--exclude` for fine-grained control over which files are processed. -``` -code2prompt --path /path/to/your/codebase --output output.md -``` +#### Best Practices: -- `--path` (required): Path to the directory containing your codebase. -- `--output` (optional): Name of the output Markdown file. If not provided, the output will be displayed in the console. -- `--gitignore` (optional): Path to a custom .gitignore file. If not provided, the tool will look for a .gitignore file in the specified directory. -- `--filter` (optional): Filter pattern to include specific files (e.g., "*.py" to include only Python files). -- `--suppress-comments` (optional): Strip comments from the code files. If not provided, comments will be included. +1. Start with broader patterns and refine as needed. +2. Test your patterns on a small subset of your project first. +3. Use the `--case-sensitive` flag if you need to distinguish between similarly named files with different cases. +4. When working with complex projects, consider using a configuration file to manage your filter and exclude patterns. -### Examples +By using the `--filter` and `--exclude` options effectively and safely (with proper quoting), you can precisely control which files are processed in your project, ensuring both accuracy and security in your command execution. -1. Generate a Markdown file for a Python project: - ``` - code2prompt --path /path/to/your/python/project --output python_project.md --filter "*.py" - ``` +## Examples -2. Generate a Markdown file for a web development project: - ``` - code2prompt --path /path/to/your/web/project --output web_project.md --filter "*.js,*.html,*.css" +1. Generate documentation for a Python library: + ```bash + code2prompt --path /path/to/library --output library_docs.md --suppress-comments --line-number --filter "*.py" ``` -3. Generate a Markdown file for a project with a custom .gitignore file: +2. Prepare a codebase summary for a code review, focusing on JavaScript and TypeScript files: + ```bash + code2prompt --path /path/to/project --filter "*.js,*.ts" --exclude "node_modules/*,dist/*" --template code_review.j2 --output code_review.md ``` - code2prompt --path /path/to/your/project --output project.md --gitignore /path/to/custom/.gitignore + +3. Create input for an AI model to suggest improvements, focusing on a specific directory: + ```bash + code2prompt --path /path/to/src/components --suppress-comments --tokens --encoding cl100k_base --output ai_input.md ``` -4. Generate a Markdown file with comments stripped from code files: +4. Analyze comment density across a multi-language project: + ```bash + code2prompt --path /path/to/project --template comment_density.j2 --output comment_analysis.md --filter "*.py,*.js,*.java" ``` - code2prompt --path /path/to/your/project --output project.md --suppress-comments + +5. Generate a prompt for a specific set of files, adding line numbers: + ```bash + code2prompt --path /path/to/important_file1.py --path /path/to/important_file2.js --line-number --output critical_files.md ``` -## Build +## Templating System -To build a distributable package of Code2Prompt using Poetry, follow these steps: +Code2Prompt supports custom output formatting using Jinja2 templates. To use a custom template: -1. Make sure you are in the project directory. +```bash +code2prompt --path /path/to/code --template /path/to/your/template.j2 +``` -2. Run the following command to build the package: - ``` - poetry build - ``` +### Creating Template Examples + +Use the `--create-templates` command to generate example templates: + +```bash +code2prompt --create-templates +``` + +This creates a `templates` directory with sample Jinja2 templates, including: - This command will create a distributable package in the `dist` directory. +- [default.j2](./code2prompt//templates/default.j2): A general-purpose template +- [analyze-code.j2](./code2prompt/templates/analyze-code.j2): For detailed code analysis +- [code-review.j2](./code2prompt/templates/code-review.j2): For thorough code reviews +- [create-readme.j2](./code2prompt/templates/create-readme.j2): To assist in generating README files +- [improve-this-prompt.j2](./code2prompt/templates/improve-this-prompt.j2): For refining AI prompts -3. You can then install the package using pip: +For full template documentation, see [Documentation Templating](./TEMPLATE.md). + +## Integration with LLM CLI + +Code2Prompt can be integrated with Simon Willison's [llm](https://github.com/simonw/llm) CLI tool for enhanced code analysis or [qllm](https://github.com/quantalogic/qllm), or for the Rust lovers [hiramu-cli](https://github.com/raphaelmansuy/hiramu-cli). + +### Installation + +```bash +pip install code2prompt llm +``` + +### Basic Usage + +1. Generate a code summary and analyze it with an LLM: + ```bash + code2prompt --path /path/to/your/project | llm "Analyze this codebase and provide insights on its structure and potential improvements" ``` - pip install dist/code2prompt-.tar.gz + +2. Process a specific file and get refactoring suggestions: + ```bash + code2prompt --path /path/to/your/script.py | llm "Suggest refactoring improvements for this code" ``` - Replace `` with the actual version number of the package. +For more advanced use cases, refer to the [Integration with LLM CLI](#integration-with-llm-cli) section in the full documentation. + +## GitHub Actions Integration + +You can integrate Code2Prompt into your GitHub Actions workflow. Here's an example: + +```yaml +name: Code Analysis +on: [push] +jobs: + analyze-code: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + pip install code2prompt llm + - name: Analyze codebase + run: | + code2prompt --path . | llm "Perform a comprehensive analysis of this codebase. Identify areas for improvement, potential bugs, and suggest optimizations." > analysis.md + - name: Upload analysis + uses: actions/upload-artifact@v2 + with: + name: code-analysis + path: analysis.md +``` -## License +## Understanding Tokens and Token Types in Code2Prompt + +Tokens are the basic units of text that language models process. They can be words, parts of words, or even punctuation marks. Different tokenizer encodings split text into tokens in various ways. Code2Prompt supports multiple token types through its `--encoding` option, with "cl100k_base" as the default. This encoding, used by models like GPT-3.5 and GPT-4, is adept at handling code and technical content. Other common encodings include "p50k_base" (used by earlier GPT-3 models) and "r50k_base" (used by models like CodeX). + +To count tokens in your generated prompt, use the `--tokens` flag: + +```bash +code2prompt --path /your/project --tokens +``` + +For a specific encoding: -Code2Prompt is released under the MIT License. See [LICENSE](./LICENCE.md) for more information. +```bash +code2prompt --path /your/project --tokens --encoding p50k_base +``` + +Understanding token counts is crucial when working with AI models that have token limits, ensuring your prompts fit within the model's context window. + +### Token Price Estimation + +Code2Prompt now includes a powerful feature for estimating token prices across various AI providers and models. Use the `--price` option in conjunction with `--tokens` to display a comprehensive breakdown of estimated costs. This feature calculates prices based on both input and output tokens, with input tokens determined by your codebase and a default of 1000 output tokens (customizable via `--output-tokens`). You can specify a particular provider or model, or view prices across all available options. This functionality helps developers make informed decisions about AI model usage and cost management. For example: + +```bash +code2prompt --path /your/project --tokens --price --provider openai --model gpt-4 +``` + +This command will analyze your project, count the tokens, and provide a detailed price estimation for OpenAI's GPT-4 model. + +![](./docs/screen-example2.png) + +## πŸ”₯ Analyzing Codebases + +code2prompt now offers a powerful feature to analyze codebases and provide a summary of file extensions. Use the `--analyze` option along with the `-p` (path) option to get an overview of your project's file composition. For example: + +``` +code2prompt --analyze -p code2prompt +``` + +Result: + +``` +.j2: 6 files +.json: 1 file +.py: 33 files +.pyc: 56 files + +Comma-separated list of extensions: +.j2,.json,.py,.pyc +``` + +This command will analyze the 'code2prompt' directory and display a summary of all file extensions found, including their counts. You can choose between two output formats: + +- Flat format (default): Lists all unique extensions alphabetically with their file counts. +- Tree-like format: Displays extensions in a directory tree structure with counts at each level. + +To use the tree-like format, add the `--format tree` option: + +``` +code2prompt --analyze -p code2prompt --format tree +``` + +Result: + +``` +└── code2prompt + β”œβ”€β”€ utils + β”‚ β”œβ”€β”€ .py + β”‚ └── __pycache__ + β”‚ └── .pyc + β”œβ”€β”€ .py + β”œβ”€β”€ core + β”‚ β”œβ”€β”€ .py + β”‚ └── __pycache__ + β”‚ └── .pyc + β”œβ”€β”€ comment_stripper + β”‚ β”œβ”€β”€ .py + β”‚ └── __pycache__ + β”‚ └── .pyc + β”œβ”€β”€ __pycache__ + β”‚ └─ .pyc + β”œβ”€β”€ templates + β”‚ └── .j2 + └── data + └── .json + +Comma-separated list of extensions: +.j2,.json,.py,.pyc +``` + +The analysis also generates a comma-separated list of file extensions, which can be easily copied and used with the `--filter` option for more targeted code processing. + +## πŸ”₯ Feature Highlight: Dynamic Variable Extraction for Prompt Generation + +`code2prompt` offers a powerful feature for dynamic variable extraction from templates, allowing for interactive and customizable prompt generation. Using the syntax `{{input:variable_name}}`, you can easily define variables that will prompt users for input during execution. + +This is particularly useful for creating flexible templates for various purposes, such as generating AI prompts for Chrome extensions. Here's an example: + +```jinja2 +# AI Prompt Generator for Chrome Extension + +Generate a prompt for an AI to create a Chrome extension with the following specifications: + +Extension Name: {{input:extension_name}} +Main Functionality: {{input:main_functionality}} +Target Audience: {{input:target_audience}} + +## Prompt: + +You are an experienced Chrome extension developer. Create a detailed plan for a Chrome extension named "{{input:extension_name}}" that {{input:main_functionality}}. This extension is designed for {{input:target_audience}}. + +Your response should include: + +1. A brief description of the extension's purpose and functionality +2. Key features (at least 3) +3. User interface design considerations +4. Potential challenges in development and how to overcome them +5. Security and privacy considerations +6. A basic code structure for the main components (manifest.json, background script, content script, etc.) + +Ensure that your plan is detailed, technically sound, and tailored to the needs of {{input:target_audience}}. + +Start from this codebase: + +---- + +## The codebase: + + +``` + +When you run `code2prompt` with this template, it will automatically detect the `{{input:variable_name}}` patterns and prompt the user to provide values for each variable (extension_name, main_functionality, and target_audience). This allows for flexible and interactive prompt generation, making it easy to create customized AI prompts for various Chrome extension ideas. + +For example, if a user inputs: +- Extension Name: "ProductivityBoost" +- Main Functionality: "tracks time spent on different websites and provides productivity insights" +- Target Audience: "professionals working from home" + +The tool will generate a tailored prompt for an AI to create a detailed plan for this specific Chrome extension. This feature is particularly useful for developers, product managers, or anyone looking to quickly generate customized AI prompts for various projects or ideas. + +## πŸ”₯ Feature Highlight "Include File" Feature + +The code2prompt project now supports a powerful "include file" feature, enhancing template modularity and reusability. + + This feature allows you to seamlessly incorporate external file content into your main template using the `{% include %}` directive. For example, in the main `analyze-code.j2` template, you can break down complex sections into smaller, manageable files: + +```jinja2 +# Elite Code Analyzer and Improvement Strategist 2.0 + +{% include 'sections/role_and_goal.j2' %} + +{% include 'sections/core_competencies.j2' %} + +## Task Breakdown + +1. Initial Assessment +{% include 'tasks/initial_assessment.j2' %} + +2. Multi-Dimensional Analysis (Utilize Tree of Thought) +{% include 'tasks/multi_dimensional_analysis.j2' %} + +// ... other sections ... +``` + +This approach allows you to organize your template structure more efficiently, improving maintainability and allowing for easy updates to specific sections without modifying the entire template. The include feature supports both relative and absolute paths, making it flexible for various project structures. By leveraging this feature, you can significantly reduce code duplication, improve template management, and create a more modular and scalable structure for your code2prompt templates. + +## Interactive Mode + +The interactive mode allows users to select files for processing in a user-friendly manner. This feature is particularly useful when dealing with large codebases or when you want to selectively include files without manually specifying each path. + +### How to Use Interactive Mode + +To activate interactive mode, use the `--interactive` or `-i` option when running the `code2prompt` command. Here's an example: + +```bash +code2prompt --path /path/to/your/project --interactive +``` + +![](./docs/screen-example3.png) + + +### Features of Interactive Mode + +- **File Selection**: Navigate through the directory structure and select files using keyboard controls. +- **Visual Feedback**: The interface provides visual cues to help you understand which files are selected or ignored. + +### Keyboard Controls + +- **Arrow Keys**: Navigate through the list of files. +- **Spacebar**: Toggle the selection of a file. +- **Enter**: Confirm your selection and proceed with the command. +- **Esc**: Exit the interactive mode without making any changes. + +This mode enhances the usability of Code2Prompt, making it easier to manage file selections in complex projects. + +## Configuration File + +Code2Prompt supports a `.code2promptrc` configuration file in JSON format for setting default options. Place this file in your project or home directory. + +Example `.code2promptrc`: + +```json +{ + "suppress_comments": true, + "line_number": true, + "encoding": "cl100k_base", + "filter": "*.py,*.js", + "exclude": "tests/*,docs/*" +} +``` + +## Troubleshooting + +1. **Issue**: Code2Prompt is not recognizing my .gitignore file. + **Solution**: Run Code2Prompt from the project root, or specify the .gitignore path with `--gitignore`. + +2. **Issue**: The generated output is too large for my AI model. + **Solution**: Use `--tokens` to check the count, and refine `--filter` or `--exclude` options. + +3. **Issue**: Some files are not being processed. + **Solution**: Check for binary files or exclusion patterns. Use `--case-sensitive` if needed. + +## Roadmap + + - [X] Interactive filtering + - [X] Include system in template to promote re-usability of sub templates. + - [X] Support of input variables + - [ ] Tokens count for Anthropic Models and other models such as LLama3 or Mistral + - [X] Cost Estimations for main LLM providers based on token count + - [ ] Integration with [qllm](https://github.com/quantalogic/qllm) (Quantalogic LLM) + - [ ] Embedding of file summary in SQL-Lite + - [ ] Intelligence selection of file based on an LLM + - [ ] Git power tools (Git diff integration / PR Assisted Review) ## Contributing -Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the [GitHub repository](https://github.com/raphaelmansuy/code2prompt). +Contributions to Code2Prompt are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. + +## License + +Code2Prompt is released under the MIT License. See the [LICENSE](LICENSE) file for details. -## Acknowledgements +--- -Code2Prompt was inspired by the need to provide better context to LLMs when asking code-related questions. We would like to thank the open-source community for their valuable contributions. +⭐ If you find Code2Prompt useful, please give us a star on GitHub! It helps us reach more developers and improve the tool. ⭐ -If you have any questions or need further assistance, please don't hesitate to reach out. Happy coding! +## Project Growth +[![Star History Chart](https://api.star-history.com/svg?repos=raphaelmansuy/code2prompt&type=Date)](https://star-history.com/#raphaelmansuy/code2prompt&Date) -Made with ❀️ by RaphΓ«l MANSUY. +Made with ❀️ by RaphaΓ«l MANSUY. Founder of [Quantalogic](https://www.quantalogic.app). Creator of [qllm](https://github.com/quantalogic/qllm). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/TEMPLATE.md b/TEMPLATE.md new file mode 100644 index 0000000..906fc22 --- /dev/null +++ b/TEMPLATE.md @@ -0,0 +1,114 @@ + +# Using the Templating System in code2prompt + +The templating system in `code2prompt` allows you to create custom output formats using Jinja2 templates. This feature is activated by using the `--template` or `-t` option when running the tool. + +## Basic Usage + +```bash +code2prompt generate --path path/to/codebase --template path/to/template.j2 --output output.md +``` + +## Available Variables + +In your Jinja2 template, you have access to the following variables: + +1. `files`: A list of dictionaries, where each dictionary contains information about a processed file. Each file dictionary includes: + - `path`: The file path (string) + - `extension`: The file extension (string) + - `language`: The inferred programming language (string) + - `size`: The file size in bytes (integer) + - `created`: The file creation timestamp (string) + - `modified`: The file modification timestamp (string) + - `content`: The file content (string) + - `no_codeblock`: A flag indicating whether to disable wrapping code inside markdown code blocks (boolean) + +2. User-defined variables: Any additional variables you define in your template using `{{ variable_name }}` syntax will be prompted for input when running the tool. + +## Template Examples + +### Example 1: Basic File Listing + +```jinja2 +# Code Analysis Report + +{% for file in files %} +## {{ file.path }} + +- Language: {{ file.language }} +- Size: {{ file.size }} bytes +- Last modified: {{ file.modified }} + +{{ file.language }} +{{ file.content }} +` + +{% endfor %} +``` + +### Example 2: Custom Project Overview + +```jinja2 +# {{ project_name }} Analysis + +Project: {{ project_name }} +Analyzed on: {{ analysis_date }} + +## File Summary + +Total files analyzed: {{ files|length }} + +{% for file in files %} +- {{ file.path }} ({{ file.language }}, {{ file.size }} bytes) +{% endfor %} + +## Detailed Code Review + +{% for file in files %} +### {{ file.path }} + +{{ file.language }} +{{ file.content }} + + +{% endfor %} +``` + +In this example, `project_name` and `analysis_date` are user-defined variables. When you run the tool with this template, it will prompt you to enter values for these variables. + +### Example 3: Language-specific Analysis + +```jinja2 +# {{ project_name }} Code Analysis + +{% set python_files = files|selectattr("language", "equalto", "python")|list %} +{% set js_files = files|selectattr("language", "equalto", "javascript")|list %} + +## Python Files ({{ python_files|length }}) + +{% for file in python_files %} +### {{ file.path }} + +{{ file.content }} + +{% endfor %} + +## JavaScript Files ({{ js_files|length }}) + +{% for file in js_files %} +### {{ file.path }} + +{{ file.content }} + +{% endfor %} +``` + +This template groups files by language and creates separate sections for Python and JavaScript files. + +## Tips for Using Templates + +1. Use Jinja2 control structures like `{% for %}`, `{% if %}`, etc., to customize the output format. +2. Utilize Jinja2 filters to manipulate data, e.g., `{{ variable|upper }}` to convert text to uppercase. +3. Create user-defined variables for dynamic content that you want to input at runtime. +4. Use the `files` list to iterate over all processed files and access their properties. +5. Remember that the `content` of each file is already processed according to the command-line options (e.g., comments stripped if `--suppress-comments` was used). diff --git a/code2prompt/commands/__init__.py b/code2prompt/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code2prompt/commands/analyze.py b/code2prompt/commands/analyze.py new file mode 100644 index 0000000..7d601a7 --- /dev/null +++ b/code2prompt/commands/analyze.py @@ -0,0 +1,70 @@ +# code2prompt/commands/analyze.py + +from pathlib import Path +from typing import Dict + +from code2prompt.commands.base_command import BaseCommand +from code2prompt.utils.analyzer import ( + analyze_codebase, + format_flat_output, + format_tree_output, + get_extension_list, +) + + +class AnalyzeCommand(BaseCommand): + """Command for analyzing the codebase structure.""" + + def execute(self) -> None: + """Execute the analyze command.""" + self.logger.info("Analyzing codebase...") + + for path in self.config.path: + self._analyze_path(Path(path)) + + self.logger.info("Analysis complete.") + + def _analyze_path(self, path: Path) -> None: + """ + Analyze a single path and output the results. + + Args: + path (Path): The path to analyze. + """ + extension_counts, extension_dirs = analyze_codebase(path) + + if not extension_counts: + self.logger.warning(f"No files found in {path}") + return + + if self.config.format == "flat": + output = format_flat_output(extension_counts) + else: + output = format_tree_output(extension_dirs) + + print(output) + + print("\nComma-separated list of extensions:") + print(get_extension_list(extension_counts)) + + if self.config.tokens: + total_tokens = self._count_tokens(extension_counts) + self.logger.info(f"Total tokens in codebase: {total_tokens}") + + def _count_tokens(self, extension_counts: Dict[str, int]) -> int: + """ + Count the total number of tokens in the codebase. + + Args: + extension_counts (Dict[str, int]): A dictionary of file extensions and their counts. + + Returns: + int: The total number of tokens. + """ + total_tokens = 0 + for _ext, count in extension_counts.items(): + # This is a simplified token count. You might want to implement a more + # sophisticated counting method based on the file type. + total_tokens += count * 100 # Assuming an average of 100 tokens per file + + return total_tokens diff --git a/code2prompt/commands/base_command.py b/code2prompt/commands/base_command.py new file mode 100644 index 0000000..fb308a9 --- /dev/null +++ b/code2prompt/commands/base_command.py @@ -0,0 +1,76 @@ +# code2prompt/commands/base_command.py + +from abc import ABC, abstractmethod +import logging +from code2prompt.config import Configuration + +class BaseCommand(ABC): + """ + Abstract base class for all commands in the code2prompt tool. + + This class defines the basic structure and common functionality + for all command classes. It ensures that each command has access + to the configuration and a logger, and defines an abstract execute + method that must be implemented by all subclasses. + + Attributes: + config (Configuration): The configuration object for the command. + logger (logging.Logger): The logger instance for the command. + """ + + def __init__(self, config: Configuration, logger: logging.Logger): + """ + Initialize the BaseCommand with configuration and logger. + + Args: + config (Configuration): The configuration object for the command. + logger (logging.Logger): The logger instance for the command. + """ + self.config = config + self.logger = logger + + @abstractmethod + def execute(self) -> None: + """ + Execute the command. + + This method must be implemented by all subclasses to define + the specific behavior of each command. + + Raises: + NotImplementedError: If the subclass does not implement this method. + """ + raise NotImplementedError("Subclasses must implement execute method") + + def log_start(self) -> None: + """ + Log the start of the command execution. + """ + self.logger.info(f"Starting execution of {self.__class__.__name__}") + + def log_end(self) -> None: + """ + Log the end of the command execution. + """ + self.logger.info(f"Finished execution of {self.__class__.__name__}") + + def handle_error(self, error: Exception) -> None: + """ + Handle and log any errors that occur during command execution. + + Args: + error (Exception): The exception that was raised. + """ + self.logger.error(f"Error in {self.__class__.__name__}: {str(error)}", exc_info=True) + + def validate_config(self) -> bool: + """ + Validate the configuration for the command. + + This method should be overridden by subclasses to perform + command-specific configuration validation. + + Returns: + bool: True if the configuration is valid, False otherwise. + """ + return True \ No newline at end of file diff --git a/code2prompt/commands/generate.py b/code2prompt/commands/generate.py new file mode 100644 index 0000000..3c6b9f9 --- /dev/null +++ b/code2prompt/commands/generate.py @@ -0,0 +1,68 @@ +""" +This module contains the GenerateCommand class, which is responsible for generating +markdown content from code files based on the provided configuration. +""" + +from typing import List, Dict, Any +from code2prompt.core.process_files import process_files +from code2prompt.core.generate_content import generate_content +from code2prompt.core.write_output import write_output +from code2prompt.utils.count_tokens import count_tokens +from code2prompt.utils.logging_utils import log_token_count +from code2prompt.utils.display_price_table import display_price_table +from code2prompt.commands.base_command import BaseCommand + + +class GenerateCommand(BaseCommand): + """Command for generating markdown content from code files.""" + + def execute(self) -> None: + """Execute the generate command.""" + self.logger.info("Generating markdown...") + file_paths = self._process_files(syntax_map=self.config.syntax_map) # Pass syntax_map here + content = self._generate_content(file_paths) + self._write_output(content) + + if self.config.price: + self.display_token_count_and_price(content) + elif self.config.tokens: + self.display_token_count(content) + + self.logger.info("Generation complete.") + + def _process_files(self, syntax_map: dict) -> List[Dict[str, Any]]: + """Process files based on the configuration.""" + all_files_data = [] + files_data = process_files( + file_paths=self.config.path, + line_number=self.config.line_number, + no_codeblock=self.config.no_codeblock, + suppress_comments=self.config.suppress_comments, + strip_body=self.config.strip_body, + syntax_map=syntax_map, # Pass syntax_map here + ) + all_files_data.extend(files_data) + return all_files_data + + def _generate_content(self, files_data: List[Dict[str, Any]]) -> str: + """Generate content from processed files data.""" + return generate_content(files_data, self.config.dict()) + + def _write_output(self, content: str) -> None: + """Write the generated content to output.""" + write_output(content, self.config.output, copy_to_clipboard=True) + + def display_token_count_and_price(self, content: str) -> None: + """Handle token counting and price calculation if enabled.""" + token_count = count_tokens(content, self.config.encoding) + model = self.config.model + provider = self.config.provider + display_price_table(token_count, provider, model, self.config.output_tokens) + log_token_count(token_count) + + + def display_token_count(self, content: str) -> None: + """Display the token count if enabled.""" + token_count = count_tokens(content, self.config.encoding) + log_token_count(token_count) + diff --git a/code2prompt/commands/interactive_selector.py b/code2prompt/commands/interactive_selector.py new file mode 100644 index 0000000..c9aa5af --- /dev/null +++ b/code2prompt/commands/interactive_selector.py @@ -0,0 +1,297 @@ +from typing import List, Dict, Set, Tuple +import os +from pathlib import Path +from prompt_toolkit import Application +from prompt_toolkit.layout.containers import VSplit, HSplit, Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.layout.layout import Layout +from prompt_toolkit.layout.scrollable_pane import ScrollablePane +from prompt_toolkit.widgets import Frame +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.styles import Style +import signal + +# Constant for terminal height adjustment +TERMINAL_HEIGHT_ADJUSTMENT = 3 + + +class InteractiveFileSelector: + """Interactive file selector.""" + + def __init__(self, paths: List[Path], selected_files: List[Path]): + self.paths: List[Path] = paths.copy() + self.start_line: int = 0 + self.cursor_position: int = 0 + self.formatted_tree: List[str] = [] + self.tree_paths: List[Path] = [] + self.tree_full_paths: List[str] = [] + self.kb = self._create_key_bindings() + self.selected_files: Set[str] = set( + [str(Path(file).resolve()) for file in selected_files] + ) + self.selection_state: Dict[str, Set[str]] = {} # State tracking for selections + self.app = self._create_application(self.kb) + + def _get_terminal_height(self) -> int: + """Get the height of the terminal.""" + return os.get_terminal_size().lines + + def _get_directory_tree(self) -> Dict[Path, Dict]: + """Get a combined directory tree for the given paths.""" + tree: Dict[Path, Dict] = {} + for path in self.paths: + current = tree # Start from the root of the tree + for part in Path(path).parts: + if part not in current: # Check if part is already in the current level + current[part] = {} # Create a new dictionary for the part + current = current[part] # Move to the next level in the tree + return tree + + def _format_tree( + self, tree: Dict[Path, Dict], indent: str = "", parent_dir: str = "" + ) -> Tuple[List[str], List[Path], List[str]]: + """Format the directory tree into a list of strings.""" + lines: List[str] = [] + tree_paths: List[Path] = [] + tree_full_paths: List[str] = [] + for i, (file_path, subtree) in enumerate(tree.items()): + is_last = i == len(tree) - 1 + prefix = "└── " if is_last else "β”œβ”€β”€ " + line = f"{indent}{prefix}{Path(file_path).name}" + lines.append(line) + resolved_path = Path(parent_dir, file_path).resolve() + tree_paths.append(resolved_path) + tree_full_paths.append( + str(resolved_path) + ) # Store the full path as a string + if subtree: + extension = " " if is_last else "β”‚ " + sub_lines, sub_tree_paths, sub_full_paths = self._format_tree( + subtree, indent + extension, str(resolved_path) + ) + lines.extend(sub_lines) + tree_paths.extend(sub_tree_paths) + tree_full_paths.extend( + sub_full_paths + ) # Merge the full paths from the subtree + return lines, tree_paths, tree_full_paths + + def _validate_cursor_position(self) -> None: + """Ensure cursor position is valid.""" + if self.cursor_position < 0: + self.cursor_position = 0 + elif self.cursor_position >= len(self.formatted_tree): + self.cursor_position = len(self.formatted_tree) - 1 + + def _get_visible_lines(self) -> int: + """Calculate the number of visible lines based on terminal height.""" + terminal_height = self._get_terminal_height() + return terminal_height - TERMINAL_HEIGHT_ADJUSTMENT # Use constant + + def _get_formatted_text(self) -> List[tuple]: + """Generate formatted text for display.""" + result = [] + # Ensure that formatted_tree and tree_paths have the same length + if len(self.formatted_tree) == len(self.tree_paths): + visible_lines = self._get_visible_lines() + # Calculate the end line for the loop + end_line = min(self.start_line + visible_lines, len(self.formatted_tree)) + for i in range(self.start_line, end_line): + line = self.formatted_tree[i] + style = "class:cursor" if i == self.cursor_position else "" + # Ensure cursor_position is valid + self._validate_cursor_position() + # Get the full path + file_path = str(self.tree_full_paths[i]) + is_dir = os.path.isdir(file_path) + # Check if the full path is selected + is_selected = file_path in self.selected_files + # Update checkbox based on selection state + checkbox = "[X]" if is_selected else " " if is_dir else "[ ]" + if file_path in self.selection_state: + if len(self.selection_state[file_path]) == len(self.tree_paths): + checkbox = "[X]" + # Append formatted line to result + result.append((style, f"{checkbox} {line}\n")) + return result + + def _toggle_file_selection(self, current_item: str) -> None: + """Toggle the selection of the current item.""" + # Convert current_item to string to use with startswith + current_item_str = str(current_item) + if current_item_str in self.selected_files: + self.selected_files.remove(current_item_str) + # Unselect all descendants + if current_item_str in self.selection_state: + for descendant in self.selection_state[current_item_str]: + self.selected_files.discard(descendant) + del self.selection_state[current_item_str] + else: + self.selected_files.add(current_item_str) + # Select all descendants + self.selection_state[current_item_str] = { + descendant + for descendant in self.tree_paths + if str(descendant).startswith(current_item_str) + } + + def _get_current_item(self) -> str: + """Get the current item based on cursor position.""" + if 0 <= self.cursor_position < len(self.tree_paths): + current_item = self.tree_full_paths[self.cursor_position] + return current_item # Return the full path + return None # Return None if no valid path is found + + def _resize_handler(self, _event) -> None: + """Handle terminal resize event.""" + self.start_line = max(0, self.cursor_position - self._get_visible_lines() + 1) + self.app.invalidate() # Invalidate the application to refresh the layout + + def run(self) -> List[Path]: + """Run the interactive file selection.""" + self._check_paths() + tree = self._get_directory_tree() + self.formatted_tree, self.tree_paths, self.tree_full_paths = self._format_tree( + tree + ) + signal.signal(signal.SIGWINCH, self._resize_handler) + self.app.run() + list_selected_files : List[Path] = [] + for f in self.selected_files: + list_selected_files.append(Path(f)) + print(list_selected_files) + return list_selected_files + + def _create_key_bindings(self) -> KeyBindings: + """Create and return key bindings for the application.""" + kb = KeyBindings() + + @kb.add("q") + def quit_application(event): + event.app.exit() + + @kb.add("up") + def move_cursor_up(_event): + if self.cursor_position > 0: + self.cursor_position -= 1 + # Update start_line if needed for scrolling + if self.cursor_position < self.start_line: + self.start_line = self.cursor_position + self._validate_cursor_position() # Validate after moving + self.app.invalidate() # Refresh the display after moving + + @kb.add("down") + def move_cursor_down(_event): + if self.cursor_position < len(self.formatted_tree) - 1: + self.cursor_position += 1 + # Update start_line if needed for scrolling + if self.cursor_position >= self.start_line + self._get_visible_lines(): + self.start_line += 1 + self._validate_cursor_position() # Validate after moving + self.app.invalidate() # Refresh the display after moving + + @kb.add("pageup") + def page_up(_event): + self.cursor_position = max( + 0, self.cursor_position - self._get_visible_lines() + ) + if self.cursor_position < self.start_line: + self.start_line = ( + self.cursor_position + ) # Adjust start_line to keep the cursor in view + self.app.invalidate() # Refresh the display after moving + + @kb.add("pagedown") + def page_down(_event): + self.cursor_position = min( + len(self.formatted_tree) - 1, + self.cursor_position + self._get_visible_lines(), + ) + if self.cursor_position >= self.start_line + self._get_visible_lines(): + self.start_line = ( + self.cursor_position - self._get_visible_lines() + 1 + ) # Adjust start_line to keep the cursor in view + self.app.invalidate() # Refresh the display after moving + + @kb.add("space") + def toggle_selection(_event): + current_item = self._get_current_item() # Get the current item as a Path + if current_item: # Ensure current_item is not None + self._toggle_file_selection( + current_item + ) # Pass the Path object directly + self.app.invalidate() # Refresh the display after toggling + + @kb.add("enter") + def confirm_selection(_event): + self.app.exit() + + return kb + + def _get_selected_files_text(self) -> str: + """Get the selected files text.""" + if self.selected_files: + return f"Selected: {len(self.selected_files)} file(s)" + return "Selected: 0 file(s): None" + + def _create_application(self, kb) -> Application: + """Create and return the application instance.""" + tree_window = Window( + content=FormattedTextControl(self._get_formatted_text, focusable=True), + width=60, + dont_extend_width=True, + wrap_lines=False, + ) + scrollable_tree = ScrollablePane(tree_window) + instructions = ( + "Instructions:\n" + "-------------\n" + "1. Use ↑ and ↓ to navigate\n" + "2. Press Space to select/deselect an item\n" + "3. Press Enter to confirm your selection\n" + "4. Press q to quit the selection process\n" + ) + layout = Layout( + VSplit( + [ + Frame(scrollable_tree, title="File Tree"), + Window(width=1, char="β”‚"), + HSplit( + [ + Window( + content=FormattedTextControl(instructions), height=5 + ), + Window(height=1), + Window( + content=FormattedTextControl( + self._get_selected_files_text + ), + height=10, + ), + ], + ), + ], + padding=1, + ) + ) + style = Style.from_dict( + { + "cursor": "bg:#00ff00 #000000", + "frame.border": "#888888", + } + ) + + return Application( + layout=layout, + key_bindings=kb, + full_screen=True, + style=style, + mouse_support=True, + ) + + def _check_paths(self) -> None: + """Check if the provided paths are valid.""" + if not self.paths or any(not path for path in self.paths): + raise ValueError( + "A valid list of paths must be provided for interactive mode." + ) diff --git a/code2prompt/comment_stripper.py b/code2prompt/comment_stripper.py deleted file mode 100644 index 3552ebe..0000000 --- a/code2prompt/comment_stripper.py +++ /dev/null @@ -1,180 +0,0 @@ -""" A collection of functions to strip comments from code strings based on the programming language. """ - -import re - - -def strip_c_style_comments(code: str) -> str: - """ - Strips C-style comments from the given code string. - Supports single-line comments (//), multi-line comments (/* */), and string literals. - - :param code: The code string to strip comments from. - :return: The code string with C-style comments removed. - """ - pattern = re.compile( - r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', - re.DOTALL | re.MULTILINE, - ) - return re.sub( - pattern, - lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", - code, - ) - - -def strip_html_style_comments(code: str) -> str: - """ - Strips HTML-style comments from the given code string. - Supports both single-line and multi-line comments (). - - :param code: The code string to strip comments from. - :return: The code string with HTML-style comments removed. - """ - pattern = re.compile(r"", re.DOTALL) - return re.sub(pattern, "", code) - - -def strip_python_style_comments(code: str) -> str: - """ - Strips Python-style comments from the given code string. - Supports single-line comments (#), multi-line comments (''' ''' or \"\"\" \"\"\"), and string literals. - - :param code: The code string to strip comments from. - :return: The code string with Python-style comments removed. - """ - pattern = re.compile( - r'(?s)#.*?$|\'\'\'.*?\'\'\'|""".*?"""|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', - re.MULTILINE, - ) - return re.sub( - pattern, - lambda match: ( - "" if match.group(0).startswith(("#", "'''", '"""')) else match.group(0) - ), - code, - ) - - -def strip_shell_style_comments(code: str) -> str: - """ - Strips shell-style comments from the given code string. - Supports single-line comments (#) and multi-line comments (: ' '). - - :param code: The code string to strip comments from. - :return: The code string with shell-style comments removed. - """ - lines = code.split("\n") - new_lines = [] - in_multiline_comment = False - - for line in lines: - if line.strip().startswith("#!"): - # Preserve shebang lines - new_lines.append(line) - elif in_multiline_comment: - if line.strip().endswith("'"): - in_multiline_comment = False - elif line.strip().startswith(": '"): - in_multiline_comment = True - elif "#" in line: - # Remove single-line comments - line = line.split("#", 1)[0] - if line.strip(): - new_lines.append(line) - else: - new_lines.append(line) - - return "\n".join(new_lines).strip() - -def strip_sql_style_comments(code: str) -> str: - """ - Strips SQL-style comments from the given code string. - Supports single-line comments (--), multi-line comments (/* */), and string literals. - - :param code: The code string to strip comments from. - :return: The code string with SQL-style comments removed. - """ - pattern = re.compile( - r'--.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', - re.DOTALL | re.MULTILINE, - ) - return re.sub( - pattern, - lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", - code, - ) - - -def strip_matlab_style_comments(code: str) -> str: - """ - Strips MATLAB-style comments from the given code string. - Supports single-line comments (%) and string literals. - - :param code: The code string to strip comments from. - :return: The code string with MATLAB-style comments removed. - """ - pattern = re.compile( - r'%.*?$|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE - ) - return re.sub( - pattern, - lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", - code, - ) - - -def strip_r_style_comments(code: str) -> str: - """ - Strips R-style comments from the given code string. - Supports single-line comments (#) and string literals. - - :param code: The code string to strip comments from. - :return: The code string with R-style comments removed. - """ - pattern = re.compile( - r'#.*?$|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE - ) - return re.sub( - pattern, - lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", - code, - ) - - -def strip_comments(code: str, language: str) -> str: - """ - Strips comments from the given code string based on the specified programming language. - - :param code: The code string to strip comments from. - :param language: The programming language of the code. - :return: The code string with comments removed. - """ - if language in [ - "c", - "cpp", - "java", - "javascript", - "csharp", - "php", - "go", - "rust", - "kotlin", - "swift", - "scala", - "dart", - ]: - return strip_c_style_comments(code) - elif language in ["python", "ruby", "perl"]: - return strip_python_style_comments(code) - elif language in ["bash", "powershell", "shell"]: - return strip_shell_style_comments(code) - elif language in ["html", "xml"]: - return strip_html_style_comments(code) - elif language in ["sql", "plsql", "tsql"]: - return strip_sql_style_comments(code) - elif language in ["matlab", "octave"]: - return strip_matlab_style_comments(code) - elif language in ["r"]: - return strip_r_style_comments(code) - else: - return code diff --git a/code2prompt/comment_stripper/__init__.py b/code2prompt/comment_stripper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code2prompt/comment_stripper/c_style.py b/code2prompt/comment_stripper/c_style.py new file mode 100644 index 0000000..da064ae --- /dev/null +++ b/code2prompt/comment_stripper/c_style.py @@ -0,0 +1,12 @@ +import re + +def strip_c_style_comments(code: str) -> str: + pattern = re.compile( + r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE, + ) + return re.sub( + pattern, + lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", + code, + ) diff --git a/code2prompt/comment_stripper/html_style.py b/code2prompt/comment_stripper/html_style.py new file mode 100644 index 0000000..e6e938d --- /dev/null +++ b/code2prompt/comment_stripper/html_style.py @@ -0,0 +1,5 @@ +import re + +def strip_html_style_comments(code: str) -> str: + pattern = re.compile(r"", re.DOTALL) + return re.sub(pattern, "", code) diff --git a/code2prompt/comment_stripper/matlab_style.py b/code2prompt/comment_stripper/matlab_style.py new file mode 100644 index 0000000..6026f89 --- /dev/null +++ b/code2prompt/comment_stripper/matlab_style.py @@ -0,0 +1,12 @@ +import re + +def strip_matlab_style_comments(code: str) -> str: + pattern = re.compile( + r'%.*?$|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE, + ) + return re.sub( + pattern, + lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", + code, + ) diff --git a/code2prompt/comment_stripper/python_style.py b/code2prompt/comment_stripper/python_style.py new file mode 100644 index 0000000..091778c --- /dev/null +++ b/code2prompt/comment_stripper/python_style.py @@ -0,0 +1,12 @@ +import re + +def strip_python_style_comments(code: str) -> str: + pattern = re.compile( + r'(?s)#.*?$|\'\'\'.*?\'\'\'|""".*?"""|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.MULTILINE, + ) + return re.sub( + pattern, + lambda match: ("" if match.group(0).startswith(("#", "'''", '"""')) else match.group(0)), + code, + ) diff --git a/code2prompt/comment_stripper/r_style.py b/code2prompt/comment_stripper/r_style.py new file mode 100644 index 0000000..db2c7a8 --- /dev/null +++ b/code2prompt/comment_stripper/r_style.py @@ -0,0 +1,12 @@ +import re + +def strip_r_style_comments(code: str) -> str: + pattern = re.compile( + r'#.*?$|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE, + ) + return re.sub( + pattern, + lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", + code, + ) diff --git a/code2prompt/comment_stripper/shell_style.py b/code2prompt/comment_stripper/shell_style.py new file mode 100644 index 0000000..7395823 --- /dev/null +++ b/code2prompt/comment_stripper/shell_style.py @@ -0,0 +1,19 @@ +def strip_shell_style_comments(code: str) -> str: + lines = code.split("\n") + new_lines = [] + in_multiline_comment = False + for line in lines: + if line.strip().startswith("#!"): # Preserve shebang lines + new_lines.append(line) + elif in_multiline_comment: + if line.strip().endswith("'"): + in_multiline_comment = False + elif line.strip().startswith(": '"): + in_multiline_comment = True + elif "#" in line: # Remove single-line comments + line = line.split("#", 1)[0] + if line.strip(): + new_lines.append(line) + else: + new_lines.append(line) + return "\n".join(new_lines).strip() diff --git a/code2prompt/comment_stripper/sql_style.py b/code2prompt/comment_stripper/sql_style.py new file mode 100644 index 0000000..10e7604 --- /dev/null +++ b/code2prompt/comment_stripper/sql_style.py @@ -0,0 +1,12 @@ +import re + +def strip_sql_style_comments(code: str) -> str: + pattern = re.compile( + r'--.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE, + ) + return re.sub( + pattern, + lambda match: match.group(0) if match.group(0).startswith(("'", '"')) else "", + code, + ) diff --git a/code2prompt/comment_stripper/strip_comments.py b/code2prompt/comment_stripper/strip_comments.py new file mode 100644 index 0000000..aa699f6 --- /dev/null +++ b/code2prompt/comment_stripper/strip_comments.py @@ -0,0 +1,55 @@ +""" +This module contains the function to strip comments from code based on the programming language. +""" + +from .c_style import strip_c_style_comments +from .html_style import strip_html_style_comments +from .python_style import strip_python_style_comments +from .shell_style import strip_shell_style_comments +from .sql_style import strip_sql_style_comments +from .matlab_style import strip_matlab_style_comments +from .r_style import strip_r_style_comments + + +def strip_comments(code: str, language: str) -> str: + """Strips comments from the given code based on the specified programming language. + + Args: + code (str): The source code from which comments will be removed. + language (str): The programming language of the source code. + + Returns: + str: The code without comments. + """ + if language in [ + "c", + "cpp", + "java", + "javascript", + "csharp", + "php", + "go", + "rust", + "kotlin", + "swift", + "scala", + "dart", + "typescript", + "typescriptreact", + "react", + ]: + return strip_c_style_comments(code) + elif language in ["python", "ruby", "perl"]: + return strip_python_style_comments(code) + elif language in ["bash", "powershell", "shell"]: + return strip_shell_style_comments(code) + elif language in ["html", "xml"]: + return strip_html_style_comments(code) + elif language in ["sql", "plsql", "tsql"]: + return strip_sql_style_comments(code) + elif language in ["matlab", "octave"]: + return strip_matlab_style_comments(code) + elif language in ["r"]: + return strip_r_style_comments(code) + else: + return code diff --git a/code2prompt/config.py b/code2prompt/config.py new file mode 100644 index 0000000..1a5258c --- /dev/null +++ b/code2prompt/config.py @@ -0,0 +1,109 @@ +# code2prompt/config.py + +from pathlib import Path +from typing import List, Optional, Dict +from pydantic import BaseModel, Field, field_validator, ValidationError + +class Configuration(BaseModel): + """ + Configuration class for code2prompt tool. + + This class uses Pydantic for data validation and settings management. + It defines all the configuration options available for the code2prompt tool. + """ + + path: List[Path] = Field(default_factory=list, description="Path(s) to the directory or file to process.") + output: Optional[Path] = Field(None, description="Name of the output Markdown file.") + gitignore: Optional[Path] = Field(None, description="Path to the .gitignore file.") + filter: Optional[str] = Field(None, description="Comma-separated filter patterns to include files.") + exclude: Optional[str] = Field(None, description="Comma-separated patterns to exclude files.") + case_sensitive: bool = Field(False, description="Perform case-sensitive pattern matching.") + suppress_comments: bool = Field(False, description="Strip comments from the code files.") + line_number: bool = Field(False, description="Add line numbers to source code blocks.") + no_codeblock: bool = Field(False, description="Disable wrapping code inside markdown code blocks.") + strip_body: bool = Field(False, description="Strip the body of the code files.") + template: Optional[Path] = Field(None, description="Path to a Jinja2 template file for custom prompt generation.") + tokens: bool = Field(False, description="Display the token count of the generated prompt.") + encoding: str = Field("cl100k_base", description="Specify the tokenizer encoding to use.") + create_templates: bool = Field(False, description="Create a templates directory with example templates.") + log_level: str = Field("INFO", description="Set the logging level.") + price: bool = Field(False, description="Display the estimated price of tokens based on provider and model.") + provider: Optional[str] = Field(None, description="Specify the provider for price calculation.") + model: Optional[str] = Field(None, description="Specify the model for price calculation.") + output_tokens: int = Field(1000, description="Specify the number of output tokens for price calculation.") + analyze: bool = Field(False, description="Analyze the codebase and provide a summary of file extensions.") + format: str = Field("flat", description="Format of the analysis output (flat or tree-like).") + interactive: bool = Field(False, description="Interactive mode to select files.") + + # Add the syntax_map attribute + syntax_map: Dict[str, str] = Field(default_factory=dict, description="Custom syntax mappings for language inference.") + + @field_validator('encoding') + @classmethod + def validate_encoding(cls, v: str) -> str: + valid_encodings = ["cl100k_base", "p50k_base", "p50k_edit", "r50k_base"] + if v not in valid_encodings: + raise ValueError(f"Invalid encoding. Must be one of: {', '.join(valid_encodings)}") + return v + + @field_validator('log_level') + @classmethod + def validate_log_level(cls, v: str) -> str: + valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + if v.upper() not in valid_levels: + raise ValueError(f"Invalid log level. Must be one of: {', '.join(valid_levels)}") + return v.upper() + + @field_validator('format') + @classmethod + def validate_format(cls, v: str) -> str: + valid_formats = ["flat", "tree"] + if v not in valid_formats: + raise ValueError(f"Invalid format. Must be one of: {', '.join(valid_formats)}") + return v + + @classmethod + def load_from_file(cls, file_path: Path) -> "Configuration": + """ + Load configuration from a file. + + Args: + file_path (Path): Path to the configuration file. + + Returns: + Configuration: Loaded configuration object. + + Raises: + FileNotFoundError: If the configuration file is not found. + ValidationError: If the configuration file is invalid. + """ + if not file_path.exists(): + raise FileNotFoundError(f"Configuration file not found: {file_path}") + + try: + with file_path.open() as f: + config_data = f.read() + return cls.model_validate_json(config_data) + except ValidationError as e: + raise ValueError(f"Invalid configuration file: {e}") + + def merge(self, cli_options: dict) -> "Configuration": + """ + Merge CLI options with the current configuration. + + Args: + cli_options (dict): Dictionary of CLI options. + + Returns: + Configuration: New configuration object with merged options. + """ + # Create a new dict with all current config values + merged_dict = self.model_dump() + + # Update with CLI options, but only if they're different from the default + for key, value in cli_options.items(): + if value is not None and value != self.model_fields[key].default: + merged_dict[key] = value + + # Create a new Configuration object with the merged options + return Configuration.model_validate(merged_dict) \ No newline at end of file diff --git a/code2prompt/contrib/body_stripper.py b/code2prompt/contrib/body_stripper.py new file mode 100644 index 0000000..f598998 --- /dev/null +++ b/code2prompt/contrib/body_stripper.py @@ -0,0 +1,32 @@ +import re + +def strip_body_contents(code: str, language: str) -> str: + """ + Extract the names of functions and classes, along with their input parameters and types (if present) from the given code. + :param code: The code string to extract information from. + :param language: The programming language of the code. + :return: A string containing the extracted information, one item per line. + """ + if language in ['python', 'ruby']: + pattern = re.compile(r'(def|class)\s+(\w+)\s*\((.*?)\)') + elif language in ['javascript', 'typescript']: + pattern = re.compile(r'(function|class)\s+(\w+)\s*\((.*?)\)') + elif language in ['java', 'c', 'cpp', 'csharp']: + pattern = re.compile(r'(?:class|interface|enum|\b(?:void|int|float|double|char|boolean|[a-zA-Z_][\w.<>[\]]*)\b)\s+(\w+)\s*(?:\((.*?)\))?') + else: + return "Unsupported language" + + matches = pattern.findall(code) + + result = [] + for match in matches: + if language in ['python', 'ruby', 'javascript', 'typescript']: + name = match[1] + params = match[2] + result.append(f"{match[0]} {name}({params})") + else: + name = match[0] + params = match[1] if len(match) > 1 else "" + result.append(f"{name}({params})") + + return "\n".join(result) \ No newline at end of file diff --git a/code2prompt/core/__init__.py b/code2prompt/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code2prompt/core/file_path_retriever.py b/code2prompt/core/file_path_retriever.py new file mode 100644 index 0000000..da0982a --- /dev/null +++ b/code2prompt/core/file_path_retriever.py @@ -0,0 +1,70 @@ +""" +This module contains the function to get file paths based on the provided options. +""" + +from pathlib import Path +from code2prompt.utils.get_gitignore_patterns import get_gitignore_patterns +from code2prompt.utils.should_process_file import should_process_file + + +def retrieve_file_paths( + file_paths: list[Path], + filter_patterns: list[str], + exclude_patterns: list[str], + case_sensitive: bool, + gitignore: list[str], +) -> list[Path]: + """ + Retrieves file paths based on the provided options. + + Args: + file_paths (list[Path]): A list of paths to retrieve. + filter_patterns (list[str]): Patterns to include. + exclude_patterns (list[str]): Patterns to exclude. + case_sensitive (bool): Whether the filtering should be case sensitive. + gitignore (list[str]): Gitignore patterns to consider. + + Returns: + list[Path]: A list of file paths that should be processed. + """ + if not file_paths: + raise ValueError("file_paths list cannot be empty.") + + retrieved_paths: list[Path] = [] + + for path in file_paths: + try: + path = Path(path) + + # Get gitignore patterns for the current path + gitignore_patterns = get_gitignore_patterns( + path.parent if path.is_file() else path, gitignore + ) + + # Add the top-level directory if it should be processed + if path.is_dir() and should_process_file( + path, + gitignore_patterns, + path.parent, + filter_patterns, + exclude_patterns, + case_sensitive, + ): + retrieved_paths.append(path) + + # Add files and directories within the top-level directory + for file_path in path.rglob("*"): + if should_process_file( + file_path, + gitignore_patterns, + path, + filter_patterns, + exclude_patterns, + case_sensitive, + ): + retrieved_paths.append(file_path) + + except (FileNotFoundError, PermissionError) as e: + print(f"Error processing path {path}: {e}") + + return retrieved_paths \ No newline at end of file diff --git a/code2prompt/core/generate_content.py b/code2prompt/core/generate_content.py new file mode 100644 index 0000000..44fd5b4 --- /dev/null +++ b/code2prompt/core/generate_content.py @@ -0,0 +1,26 @@ +from code2prompt.core.template_processor import get_user_inputs, load_template, process_template +from code2prompt.utils.generate_markdown_content import generate_markdown_content + + +def generate_content(files_data, options): + """ + Generate content based on the provided files data and options. + + This function either processes a Jinja2 template with the given files data and user inputs + or generates markdown content directly from the files data, depending on whether a + template option is provided. + + Args: + files_data (list): A list of dictionaries containing processed file data. + options (dict): A dictionary containing options such as template path and whether + to wrap code inside markdown code blocks. + + Returns: + str: The generated content as a string, either from processing a template or + directly generating markdown content. + """ + if options['template']: + template_content = load_template(options['template']) + user_inputs = get_user_inputs(template_content) + return process_template(template_content, files_data, user_inputs, options['template']) + return generate_markdown_content(files_data, options['no_codeblock']) \ No newline at end of file diff --git a/code2prompt/core/process_file.py b/code2prompt/core/process_file.py new file mode 100644 index 0000000..64bad32 --- /dev/null +++ b/code2prompt/core/process_file.py @@ -0,0 +1,64 @@ +""" +This module contains the function to process a file and extract its metadata and content. +""" + +from pathlib import Path +from datetime import datetime + +from code2prompt.utils.add_line_numbers import add_line_numbers +from code2prompt.utils.language_inference import infer_language +from code2prompt.comment_stripper.strip_comments import strip_comments +from code2prompt.contrib.body_stripper import strip_body_contents + +def process_file( + file_path: Path, suppress_comments: bool, line_number: bool, no_codeblock: bool, strip_body: bool, syntax_map: dict +): + """ + Processes a given file to extract its metadata and content. + + Parameters: + - file_path (Path): The path to the file to be processed. + - suppress_comments (bool): Flag indicating whether to remove comments from the file content. + - line_number (bool): Flag indicating whether to add line numbers to the file content. + - no_codeblock (bool): Flag indicating whether to disable wrapping code inside markdown code blocks. + - syntax_map (dict): Custom syntax mappings for language inference. + + Returns: + dict: A dictionary containing the file information and content. + """ + file_extension = file_path.suffix + file_size = file_path.stat().st_size + file_creation_time = datetime.fromtimestamp(file_path.stat().st_ctime).strftime( + "%Y-%m-%d %H:%M:%S" + ) + file_modification_time = datetime.fromtimestamp(file_path.stat().st_mtime).strftime( + "%Y-%m-%d %H:%M:%S" + ) + + try: + with file_path.open("r", encoding="utf-8") as f: + file_content = f.read() + + language = infer_language(file_path.name, syntax_map) + + if suppress_comments and language != "unknown": + file_content = strip_comments(file_content, language) + + if strip_body: + file_content = strip_body_contents(file_content, language) + + if line_number: + file_content = add_line_numbers(file_content) + except UnicodeDecodeError: + return None + + return { + "path": str(file_path), + "extension": file_extension, + "language": language, + "size": file_size, + "created": file_creation_time, + "modified": file_modification_time, + "content": file_content, + "no_codeblock": no_codeblock, + } diff --git a/code2prompt/core/process_files.py b/code2prompt/core/process_files.py new file mode 100644 index 0000000..b2b3160 --- /dev/null +++ b/code2prompt/core/process_files.py @@ -0,0 +1,47 @@ +""" +This module contains functions for processing files and directories. +""" + +from pathlib import Path +from typing import List, Dict, Any +from code2prompt.core.process_file import process_file + + +def process_files( + file_paths: List[Path], + line_number: bool, + no_codeblock: bool, + suppress_comments: bool, + strip_body: bool, + syntax_map: dict # Add this parameter +) -> List[Dict[str, Any]]: + """ + Processes files or directories based on the provided paths. + + Args: + options (dict): A dictionary containing options such as paths, gitignore patterns, + and flags for processing files. + + Returns: + list: A list of dictionaries containing processed file data. + """ + files_data = [] + + # Test file paths if List[Path] type + if not (isinstance(file_paths, list) and all(isinstance(path, Path) for path in file_paths)): + raise ValueError("file_paths must be a list of Path objects") + + # Use get_file_paths to retrieve all file paths to process + for path in file_paths: + result = process_file( + file_path=path, + suppress_comments=suppress_comments, + line_number=line_number, + no_codeblock=no_codeblock, + strip_body=strip_body, + syntax_map=syntax_map # Ensure this is being passed + ) + if result: + files_data.append(result) + + return files_data diff --git a/code2prompt/core/template_processor.py b/code2prompt/core/template_processor.py new file mode 100644 index 0000000..c6002de --- /dev/null +++ b/code2prompt/core/template_processor.py @@ -0,0 +1,112 @@ +import os +from jinja2 import Environment +from jinja2 import TemplateNotFound +from code2prompt.utils.include_loader import CircularIncludeError, IncludeLoader +from code2prompt.utils.logging_utils import log_error +from prompt_toolkit import prompt +import re + + +def load_template(template_path): + """ + Load a template file from the given path. + + Args: + template_path (str): The path to the template file. + + Returns: + str: The contents of the template file. + + Raises: + IOError: If there is an error loading the template file. + """ + try: + with open(template_path, "r", encoding="utf-8") as file: + return file.read() + except IOError as e: + raise IOError(f"Error loading template file: {e}") from e + + +def get_user_inputs(template_content): + """ + Extracts user inputs from a template content. + + Args: + template_content (str): The content of the template. + + Returns: + dict: A dictionary containing the user inputs, where the keys are the variable names and the values are the user-entered values. + """ + pattern = r"{{\s*input:([^{}]+?)\s*}}" + matches = re.finditer(pattern, template_content) + + user_inputs = {} + for match in matches: + var_name = match.group(1).strip() + if var_name and var_name not in user_inputs: + user_inputs[var_name] = prompt(f"Enter value for {var_name}: ") + + return user_inputs + + +def replace_input_placeholders(template_content, user_inputs): + """ + Replaces input placeholders in the template content with user inputs. + + Args: + template_content (str): The content of the template. + user_inputs (dict): A dictionary containing user inputs. + + Returns: + str: The template content with input placeholders replaced by user inputs. + """ + pattern = r"{{\s*input:([^{}]+?)\s*}}" + + def replace_func(match): + var_name = match.group(1).strip() + return user_inputs.get(var_name, "") + + return re.sub(pattern, replace_func, template_content) + + +def process_template(template_content, files_data, user_inputs, template_path): + """ + Process a template by replacing input placeholders with user-provided values and rendering the template. + + Args: + template_content (str): The content of the template to be processed. + files_data (dict): A dictionary containing data for files that may be referenced in the template. + user_inputs (dict): A dictionary containing user-provided values for input placeholders in the template. + template_path (str): The path to the template file. + + Returns: + str: The processed template content with input placeholders replaced and rendered. + + Raises: + TemplateNotFound: If the template file is not found at the specified path. + CircularIncludeError: If a circular include is detected in the template. + Exception: If there is an error processing the template. + + """ + try: + template_dir = os.path.dirname(template_path) + env = Environment( + loader=IncludeLoader(template_dir), + autoescape=True, + keep_trailing_newline=True, + ) + # Replace input placeholders with user-provided values + processed_content = replace_input_placeholders(template_content, user_inputs) + template = env.from_string(processed_content) + return template.render(files=files_data, **user_inputs) + except TemplateNotFound as e: + log_error( + f"Template file not found: {e.name}. Please check the path and ensure the file exists." + ) + return None + except CircularIncludeError as e: + log_error(f"Circular include detected: {str(e)}") + return None + except IOError as e: + log_error(f"Error processing template: {e}") + return None diff --git a/code2prompt/core/write_output.py b/code2prompt/core/write_output.py new file mode 100644 index 0000000..caea1cd --- /dev/null +++ b/code2prompt/core/write_output.py @@ -0,0 +1,44 @@ +from pathlib import Path +import click +import pyperclip +from code2prompt.utils.logging_utils import ( + log_output_created, + log_error, + log_clipboard_copy, + log_token_count, +) + +def write_output(content, output_path, copy_to_clipboard=True, token_count=None): + """ + Writes the generated content to a file or prints it to the console, + logs the token count if provided, and copies the content to the clipboard. + + Parameters: + - content (str): The content to be written, printed, and copied. + - output_path (str): The path to the file where the content should be written. + If None, the content is printed to the console. + - copy_to_clipboard (bool): Whether to copy the content to the clipboard. + - token_count (int, optional): The number of tokens in the content. + + Returns: None + """ + if output_path: + try: + with Path(output_path).open("w", encoding="utf-8") as output_file: + output_file.write(content) + log_output_created(output_path) + except IOError as e: + log_error(f"Error writing to output file: {e}") + else: + click.echo(content) + + if token_count is not None: + log_token_count(token_count) + + if copy_to_clipboard: + try: + pyperclip.copy(content) + log_clipboard_copy(success=True) + except Exception as _e: + log_clipboard_copy(success=False) + \ No newline at end of file diff --git a/code2prompt/data/token_price.json b/code2prompt/data/token_price.json new file mode 100644 index 0000000..efe8270 --- /dev/null +++ b/code2prompt/data/token_price.json @@ -0,0 +1,260 @@ +{ + "description": "This file contains the price of tokens for different models. Prices are in USD for 1000 tokens.", + "providers": [ + { + "name": "OpenAI", + "models": [ + { + "name": "GPT-4o", + "input_price": 0.005, + "output_price": 0.015 + }, + { + "name": "GPT4o-mini", + "input_price": 0.000015, + "output_price": 0.00006 + }, + { + "name": "GPT-4 (8K)", + "input_price": 0.03, + "output_price": 0.06 + }, + { + "name": "GPT-4 Turbo", + "input_price": 0.01, + "output_price": 0.03 + }, + { + "name": "GPT-3.5-turbo", + "input_price": 0.0005, + "output_price": 0.0015 + } + ] + }, + { + "name": "Anthropic", + "models": [ + { + "name": "Claude 3 (Opus)", + "input_price": 0.015, + "output_price": 0.075 + }, + { + "name": "Claude 3.5 (Sonnet)", + "input_price": 0.003, + "output_price": 0.015 + }, + { + "name": "Claude 3 (Haiku)", + "input_price": 0.00025, + "output_price": 0.00125 + } + ] + }, + { + "name": "Google", + "models": [ + { + "name": "Gemini 1.5 Pro", + "input_price": 0.0035, + "output_price": 0.007 + }, + { + "name": "Gemini 1.5 Flash", + "input_price": 0.00035, + "output_price": 0.0007 + } + ] + }, + { + "name": "Groq", + "models": [ + { + "name": "Llama 3 70b", + "input_price": 0.00059, + "output_price": 0.00079 + }, + { + "name": "Mixtral 8x7B", + "input_price": 0.00024, + "output_price": 0.00024 + } + ] + }, + { + "name": "Replicate", + "models": [ + { + "name": "Llama 3 70b", + "input_price": 0.00065, + "output_price": 0.00275 + }, + { + "name": "Mixtral 8x7B", + "input_price": 0.0003, + "output_price": 0.001 + } + ] + }, + { + "name": "Mistral", + "models": [ + { + "name": "mistral-large-2402", + "input_price": 0.004, + "output_price": 0.012 + }, + { + "name": "codestral-2405", + "input_price": 0.001, + "output_price": 0.003 + }, + { + "name": "Mixtral 8x22B", + "input_price": 0.002, + "output_price": 0.006 + }, + { + "name": "Mixtral 8x7B", + "input_price": 0.0007, + "output_price": 0.0007 + } + ] + }, + { + "name": "Together.AI", + "models": [ + { + "name": "Mixtral 8x7B", + "input_price": 0.0006, + "output_price": 0.0006 + }, + { + "name": "Llama 3 70b", + "input_price": 0.0009, + "output_price": 0.0009 + } + ] + }, + { + "name": "Perplexity", + "models": [ + { + "name": "Llama 3 70b", + "input_price": 0.001, + "output_price": 0.001 + }, + { + "name": "Mixtral 8x7B", + "input_price": 0.0006, + "output_price": 0.0006 + } + ] + }, + { + "name": "Cohere", + "models": [ + { + "name": "Command R+", + "input_price": 0.003, + "output_price": 0.015 + }, + { + "name": "Command R", + "input_price": 0.0005, + "output_price": 0.0015 + } + ] + }, + { + "name": "Deepseek", + "models": [ + { + "name": "deepseek-chat", + "input_price": 0.00014, + "output_price": 0.00028 + }, + { + "name": "deepseek-coder", + "input_price": 0.00014, + "output_price": 0.00028 + } + ] + }, + { + "name": "Anyscale", + "models": [ + { + "name": "Mixtral 8x7B", + "input_price": 0.0005, + "output_price": 0.0005 + }, + { + "name": "Llama 3 70b", + "input_price": 0.001, + "output_price": 0.001 + } + ] + }, + { + "name": "IBM WatsonX", + "models": [ + { + "name": "Llama 3 70b", + "input_price": 0.0018, + "output_price": 0.0018 + } + ] + }, + { + "name": "Fireworks", + "models": [ + { + "name": "Llama 3 70b", + "input_price": 0.0009, + "output_price": 0.0009 + }, + { + "name": "Mixtral 8x7B", + "input_price": 0.0005, + "output_price": 0.0005 + } + ] + }, + { + "name": "01.ai", + "models": [ + { + "name": "Yi-Large", + "input_price": 0.003, + "output_price": 0.003 + } + ] + }, + { + "name": "Writer", + "models": [ + { + "name": "Palmyra X 003", + "input_price": 0.0075, + "output_price": 0.0225 + }, + { + "name": "Palmyra X 32k", + "input_price": 0.001, + "output_price": 0.002 + }, + { + "name": "Palmyra X 002", + "input_price": 0.001, + "output_price": 0.002 + }, + { + "name": "Palmyra X 002 32k", + "input_price": 0.001, + "output_price": 0.002 + } + ] + } + ] + } \ No newline at end of file diff --git a/code2prompt/language_inference.py b/code2prompt/language_inference.py index 8c8c53b..e69de29 100644 --- a/code2prompt/language_inference.py +++ b/code2prompt/language_inference.py @@ -1,61 +0,0 @@ -""" This module contains the function to infer the programming language based on the file extension. """ - -import os - - -def infer_language(filename: str) -> str: - """ - Infers the programming language based on the file extension. - - :param filename: The name of the file. - :return: The inferred programming language. - """ - _, extension = os.path.splitext(filename) - extension = extension.lower() - - if extension in [".c", ".h"]: - return "c" - elif extension in [".cpp", ".hpp", ".cc", ".cxx"]: - return "cpp" - elif extension in [".java"]: - return "java" - elif extension in [".js", ".jsx"]: - return "javascript" - elif extension in [".cs"]: - return "csharp" - elif extension in [".php"]: - return "php" - elif extension in [".go"]: - return "go" - elif extension in [".rs"]: - return "rust" - elif extension in [".kt"]: - return "kotlin" - elif extension in [".swift"]: - return "swift" - elif extension in [".scala"]: - return "scala" - elif extension in [".dart"]: - return "dart" - elif extension in [".py"]: - return "python" - elif extension in [".rb"]: - return "ruby" - elif extension in [".pl", ".pm"]: - return "perl" - elif extension in [".sh"]: - return "bash" - elif extension in [".ps1"]: - return "powershell" - elif extension in [".html", ".htm"]: - return "html" - elif extension in [".xml"]: - return "xml" - elif extension in [".sql"]: - return "sql" - elif extension in [".m"]: - return "matlab" - elif extension in [".r"]: - return "r" - else: - return "unknown" diff --git a/code2prompt/main.py b/code2prompt/main.py index 6388047..9312b34 100644 --- a/code2prompt/main.py +++ b/code2prompt/main.py @@ -1,94 +1,32 @@ -""" Main module for the code2prompt package. """ +"""Main module for the code2prompt CLI tool.""" -from datetime import datetime +import logging from pathlib import Path -from fnmatch import fnmatch import click - -from code2prompt.language_inference import infer_language -from code2prompt.comment_stripper import strip_comments - - -def parse_gitignore(gitignore_path): - """Parse the .gitignore file and return a set of patterns.""" - if not gitignore_path.exists(): - return set() - - with gitignore_path.open("r", encoding="utf-8") as file: - patterns = set( - line.strip() for line in file if line.strip() and not line.startswith("#") - ) - return patterns - - -def is_ignored(file_path: Path, gitignore_patterns: list, base_path: Path) -> bool: - """ - Check if a file path matches any pattern in the .gitignore file. - - Args: - file_path (Path): The path of the file being checked. - gitignore_patterns (list): A list of patterns from the .gitignore file. - base_path (Path): The base path of the repository. - - Returns: - bool: True if the file path matches any pattern, False otherwise. - """ - relative_path = file_path.relative_to(base_path) - - for pattern in gitignore_patterns: - pattern = pattern.rstrip("/") # Remove trailing slash from the pattern - - if pattern.startswith("/"): - if fnmatch(str(relative_path), pattern[1:]): - return True - if fnmatch(str(relative_path.parent), pattern[1:]): - return True - else: - for path in relative_path.parents: - if fnmatch(str(path / relative_path.name), pattern): - return True - if fnmatch(str(path), pattern): - return True - if fnmatch(str(relative_path), pattern): - return True - - return False - - -def is_filtered(file_path, filter_pattern): - """Check if a file path matches the filter pattern.""" - return fnmatch(file_path.name, filter_pattern) - - -def is_binary(file_path): - """ - Determines if the specified file is a binary file. - - Args: - file_path (str): The path to the file to check. - - Returns: - bool: True if the file is binary, False otherwise. - """ - try: - with open(file_path, "rb") as file: - # Read a small portion of the file - chunk = file.read(1024) - # A file is considered binary if it contains a null byte - return b"\x00" in chunk - except IOError: - # Handle the exception if the file cannot be opened - print(f"Error: The file at {file_path} could not be opened.") - return False - - -@click.command() +from code2prompt.commands.analyze import AnalyzeCommand +from code2prompt.commands.generate import GenerateCommand +from code2prompt.config import Configuration +from code2prompt.utils.logging_utils import setup_logger +from code2prompt.commands.interactive_selector import InteractiveFileSelector +from code2prompt.core.file_path_retriever import retrieve_file_paths +from code2prompt.version import VERSION + + +@click.group(invoke_without_command=True) +@click.version_option( + VERSION, "-v", "--version", message="code2prompt version %(version)s" +) +@click.option( + "--config", + type=click.Path(exists=True, dir_okay=False), + help="Path to configuration file", +) @click.option( "--path", "-p", type=click.Path(exists=True), - required=True, - help="Path to the directory to navigate.", + multiple=True, + help="Path(s) to the directory or file to process.", ) @click.option( "--output", "-o", type=click.Path(), help="Name of the output Markdown file." @@ -100,78 +38,200 @@ def is_binary(file_path): help="Path to the .gitignore file.", ) @click.option( - "--filter", "-f", type=str, help='Filter pattern to include files (e.g., "*.py").' + "--filter", + "-f", + type=str, + help='Comma-separated filter patterns to include files (e.g., "*.py,*.js").', +) +@click.option( + "--interactive", + "-i", + is_flag=True, + help="Interactive mode to select files.", +) +@click.option( + "--exclude", + "-e", + type=str, + help='Comma-separated patterns to exclude files (e.g., "*.txt,*.md").', +) +@click.option( + "--case-sensitive", is_flag=True, help="Perform case-sensitive pattern matching." ) @click.option( "--suppress-comments", "-s", is_flag=True, help="Strip comments from the code files.", - default=False, -) -def create_markdown_file(path, output, gitignore, filter, suppress_comments): - """Create a Markdown file with the content of files in a directory.""" - content = [] - table_of_contents = [] - - path = Path(path) - gitignore_path = Path(gitignore) if gitignore else path / ".gitignore" - gitignore_patterns = parse_gitignore(gitignore_path) - gitignore_patterns.add(".git") - - for file_path in path.rglob("*"): - if ( - file_path.is_file() - and not is_ignored(file_path, gitignore_patterns, path) - and (not filter or is_filtered(file_path, filter)) - and not is_binary(file_path) - ): - file_extension = file_path.suffix - file_size = file_path.stat().st_size - file_creation_time = datetime.fromtimestamp( - file_path.stat().st_ctime - ).strftime("%Y-%m-%d %H:%M:%S") - file_modification_time = datetime.fromtimestamp( - file_path.stat().st_mtime - ).strftime("%Y-%m-%d %H:%M:%S") - - try: - with file_path.open("r", encoding="utf-8") as f: - file_content = f.read() - if suppress_comments: - language = infer_language(file_path.name) - if language != "unknown": - file_content = strip_comments(file_content, language) - except UnicodeDecodeError: - # Ignore files that cannot be decoded - continue - - file_info = f"## File: {file_path}\n\n" - file_info += f"- Extension: {file_extension}\n" - file_info += f"- Size: {file_size} bytes\n" - file_info += f"- Created: {file_creation_time}\n" - file_info += f"- Modified: {file_modification_time}\n\n" - - file_code = f"### Code\n```{file_extension}\n{file_content}\n```\n\n" - - content.append(file_info + file_code) - table_of_contents.append( - f"- [{file_path}](#{file_path.as_posix().replace('/', '')})\n" - ) - - markdown_content = ( - "# Table of Contents\n" + "".join(table_of_contents) + "\n" + "".join(content) - ) - - if output: - output_path = Path(output) - with output_path.open("w", encoding="utf-8") as md_file: - md_file.write(markdown_content) - click.echo(f"Markdown file '{output_path}' created successfully.") +) +@click.option( + "--line-number", "-ln", is_flag=True, help="Add line numbers to source code blocks." +) +@click.option( + "--no-codeblock", + is_flag=True, + help="Disable wrapping code inside markdown code blocks.", +) +@click.option( + "--strip-body", + is_flag=True, + help="Remove body of functions, classes, and methods from the code files.", +) +@click.option( + "--template", + "-t", + type=click.Path(exists=True), + help="Path to a Jinja2 template file for custom prompt generation.", +) +@click.option( + "--tokens", is_flag=True, help="Display the token count of the generated prompt." +) +@click.option( + "--encoding", + type=click.Choice(["cl100k_base", "p50k_base", "p50k_edit", "r50k_base"]), + default="cl100k_base", + help="Specify the tokenizer encoding to use.", +) +@click.option( + "--create-templates", + is_flag=True, + help="Create a templates directory with example templates.", +) +@click.option( + "--log-level", + type=click.Choice( + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False + ), + default="WARNING", + help="Set the logging level.", +) +@click.option( + "--price", + is_flag=True, + help="Display the estimated price of tokens based on provider and model.", +) +@click.option( + "--provider", type=str, help="Specify the provider for price calculation." +) +@click.option("--model", type=str, help="Specify the model for price calculation.") +@click.option( + "--output-tokens", + type=int, + default=1000, + help="Specify the number of output tokens for price calculation.", +) +@click.option( + "--syntax-map", + type=str, + help="Comma-separated list of extension:synonym mappings for syntax highlighting." +) +@click.pass_context +def cli(ctx, config, path, **generate_options): + """code2prompt CLI tool""" + ctx.obj = {} + if config: + ctx.obj["config"] = Configuration.load_from_file(Path(config)) + else: + ctx.obj["config"] = Configuration() # This will now have syntax_map initialized + + logging.info("CLI initialized with config: %s", ctx.obj["config"]) + + if ctx.invoked_subcommand is None: + ctx.invoke(generate, path=path, **generate_options) + + +@cli.command() +@click.option( + "--path", + "-p", + type=click.Path(exists=True), + multiple=True, + help="Path(s) to the directory or file to process.", +) +@click.option( + "--output", "-o", type=click.Path(), help="Name of the output Markdown file." +) +@click.pass_context +def generate(ctx, **options): + """Generate markdown from code files""" + + # Parse the syntax_map option into a dictionary + if options.get('syntax_map'): + syntax_map = {} + for mapping in options['syntax_map'].split(','): + ext, syntax = mapping.split(':') + syntax_map['.' + ext.strip()] = syntax.strip() # Add a dot before the extension + options['syntax_map'] = syntax_map # Replace the string with the dictionary + + config = ctx.obj["config"].merge(options) + logger = setup_logger(level=config.log_level) + + selected_paths: list[Path] = [Path(p) for p in config.path] + + # Check if selected_paths is empty before proceeding + if not selected_paths: + logging.error("No file paths provided. Please specify valid paths.") + return # Exit the function if no paths are provided + + filter_patterns: list[str] = config.filter.split(",") if config.filter else [] + exclude_patterns: list[str] = config.exclude.split(",") if config.exclude else [] + case_sensitive: bool = config.case_sensitive + gitignore: str = config.gitignore + + # Handle both directory and file inputs + filtered_paths = [] + for path in selected_paths: + if path.is_dir(): + filtered_paths.extend(retrieve_file_paths( + file_paths=[path], + gitignore=gitignore, + filter_patterns=filter_patterns, + exclude_patterns=exclude_patterns, + case_sensitive=case_sensitive, + )) + elif path.is_file(): + filtered_paths.append(path) + + if filtered_paths and config.interactive: + file_selector = InteractiveFileSelector(filtered_paths, filtered_paths) + filtered_selected_path = file_selector.run() + config.path = filtered_selected_path else: - click.echo(markdown_content) + config.path = filtered_paths + + command = GenerateCommand(config, logger) + command.execute() + + logger.info("Markdown generation completed.") + + +@cli.command() +@click.option( + "--path", + "-p", + type=click.Path(exists=True), + multiple=True, + help="Path(s) to analyze.", +) +@click.option( + "--format", + type=click.Choice(["flat", "tree"]), + default="flat", + help="Format of the analysis output.", +) +@click.pass_context +def analyze(ctx, **options): + """Analyze codebase structure""" + config = ctx.obj["config"].merge(options) + logger = setup_logger(level=config.log_level) + logger.info("Analyzing codebase with options: %s", options) + + command = AnalyzeCommand(config, logger) + command.execute() + + logger.info("Codebase analysis completed.") -if __name__ == "__main__": - # pylint: disable=no-value-for-parameter - create_markdown_file() +def get_directory_tree(path): + """Retrieve a list of files and directories in a given path.""" + return [p.name for p in Path(path).iterdir() if p.is_file() or p.is_dir()] diff --git a/code2prompt/print_help.py b/code2prompt/print_help.py new file mode 100644 index 0000000..249ef61 --- /dev/null +++ b/code2prompt/print_help.py @@ -0,0 +1,67 @@ +from code2prompt.version import VERSION + + +import click + + +def print_help(_ctx): + """Print comprehensive help information.""" + click.echo(click.style("code2prompt CLI Tool", fg="green", bold=True)) + click.echo(f"Version: {VERSION}\n") + + click.echo(click.style("Description:", fg="yellow", bold=True)) + click.echo("code2prompt is a powerful CLI tool for generating markdown documentation from code files and analyzing codebase structure.\n") + + click.echo(click.style("Usage:", fg="yellow", bold=True)) + click.echo(" code2prompt [OPTIONS] COMMAND [ARGS]...\n") + + click.echo(click.style("Commands:", fg="yellow", bold=True)) + click.echo(" generate Generate markdown from code files") + click.echo(" analyze Analyze codebase structure\n") + + click.echo(click.style("Global Options:", fg="yellow", bold=True)) + click.echo(" --config PATH Path to configuration file") + click.echo(" -v, --version Show the version and exit") + click.echo(" --help Show this message and exit\n") + + click.echo(click.style("Generate Command Options:", fg="yellow", bold=True)) + click.echo(" -p, --path PATH Path(s) to the directory or file to process") + click.echo(" -o, --output PATH Name of the output Markdown file") + click.echo(" -g, --gitignore PATH Path to the .gitignore file") + click.echo(" -f, --filter TEXT Comma-separated filter patterns to include files") + click.echo(" -e, --exclude TEXT Comma-separated patterns to exclude files") + click.echo(" --case-sensitive Perform case-sensitive pattern matching") + click.echo(" -s, --suppress-comments Strip comments from the code files") + click.echo(" -ln, --line-number Add line numbers to source code blocks") + click.echo(" --no-codeblock Disable wrapping code inside markdown code blocks") + click.echo(" -t, --template PATH Path to a Jinja2 template file for custom prompt generation") + click.echo(" --tokens Display the token count of the generated prompt") + click.echo(" --encoding [cl100k_base|p50k_base|p50k_edit|r50k_base]") + click.echo(" Specify the tokenizer encoding to use") + click.echo(" --create-templates Create a templates directory with example templates") + click.echo(" --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]") + click.echo(" Set the logging level") + click.echo(" --price Display the estimated price of tokens") + click.echo(" --provider TEXT Specify the provider for price calculation") + click.echo(" --model TEXT Specify the model for price calculation") + click.echo(" --output-tokens INTEGER Specify the number of output tokens for price calculation\n") + + click.echo(click.style("Analyze Command Options:", fg="yellow", bold=True)) + click.echo(" -p, --path PATH Path(s) to analyze") + click.echo(" --format [flat|tree] Format of the analysis output\n") + + click.echo(click.style("Examples:", fg="yellow", bold=True)) + click.echo(" code2prompt generate -p ./src") + click.echo(" code2prompt analyze -p ./src --format tree") + click.echo(" code2prompt generate -p ./src -o output.md --price --provider openai --model gpt-3.5-turbo\n") + + click.echo(click.style("Note:", fg="red", bold=True)) + click.echo("🚨 code2prompt 0.7.0 is a major version update from code2doc.") + click.echo("πŸ€– Starting with version 0.7.0, you must use 'code2prompt generate' to generate content from code files.") + click.echo() + click.echo(click.style("For more information, visit:", fg="cyan")) + click.echo("https://github.com/raphaelmansuy/code2prompt") + click.echo() + click.echo("Created with ❀️ by Raphael Mansuy") + + \ No newline at end of file diff --git a/code2prompt/templates/analyze-code.j2 b/code2prompt/templates/analyze-code.j2 new file mode 100644 index 0000000..8fc596f --- /dev/null +++ b/code2prompt/templates/analyze-code.j2 @@ -0,0 +1,114 @@ +# Elite Code Analyzer and Improvement Strategist 2.0 + +## Role and Goal +You are a world-class software architect and code quality expert with decades of experience across various programming languages, paradigms, and industries. Your mission is to analyze the provided codebase comprehensively, uncover its strengths and weaknesses, and develop a strategic improvement plan that balances immediate gains with long-term sustainability. + +## Core Competencies +- Mastery of software architecture principles, design patterns, and best practices across multiple paradigms (OOP, functional, etc.) +- Deep expertise in performance optimization, security hardening, and scalability enhancement for both monolithic and distributed systems +- Proven track record in successful large-scale refactoring and technical debt reduction +- Cutting-edge knowledge of modern development frameworks, cloud technologies, and DevOps practices +- Strong understanding of collaborative development processes and team dynamics + +## Task Breakdown + +1. Initial Assessment + - Identify the programming language(s), frameworks, and overall architecture + - Determine the scale and complexity of the codebase + - Assess the development environment and team structure + +2. Multi-Dimensional Analysis (Utilize Tree of Thought) + a. Functionality and Business Logic + b. Architectural Design and Patterns + c. Code Quality and Maintainability + d. Performance and Scalability + e. Security and Data Protection + f. Testing and Quality Assurance + g. DevOps and Deployment Processes + h. Documentation and Knowledge Management + +3. Improvement Identification (Apply Chain of Thought) + For each analyzed dimension: + - Describe the current state + - Envision the ideal state + - Identify the gap between current and ideal + - Generate potential improvements, considering: + - Short-term quick wins + - Medium-term enhancements + - Long-term strategic changes + +4. Holistic Evaluation (Implement Ensemble Prompting) + Compile and synthesize insights from multiple perspectives: + - Senior Developer: Code quality and maintainability + - DevOps Engineer: Scalability and operational efficiency + - Security Specialist: Vulnerability assessment and risk mitigation + - Product Manager: Feature delivery and business value + - End-user: Usability and performance perception + +5. Strategic Improvement Plan (Use Step-by-Step Reasoning) + Develop a comprehensive plan that: + - Prioritizes improvements based on: + - Impact on system quality and business value + - Implementation complexity and risk + - Resource requirements and availability + - Interdependencies between improvements + - Balances quick wins with long-term architectural enhancements + - Considers team dynamics and skill development needs + - Incorporates continuous improvement and feedback loops + +## Output Format + + +[Present as a markdown checklist, categorized by analysis dimensions] + + + +[Structured improvement plan with clear sections for immediate actions, short-term goals, and long-term vision] + + +## Additional Instructions + +- Tailor your analysis and recommendations to the specific programming languages and paradigms used in the codebase +- Use industry-standard metrics and benchmarks to support your analysis where applicable +- Provide concrete examples or pseudo-code to illustrate complex concepts or proposed changes +- Address potential challenges in implementing improvements, including team resistance or resource constraints +- Suggest collaborative approaches and tools to facilitate the improvement process +- Consider the impact of proposed changes on the entire software development lifecycle + +## Logging Level Configuration + +To configure the log level in the command line, you can use the `--log-level` option when running the `code2prompt` command. This option allows you to specify the desired logging level, such as DEBUG, INFO, WARNING, ERROR, or CRITICAL. + +## Reflection and Continuous Improvement + +After completing the analysis and plan: +- Identify areas where the analysis could be deepened with additional tools or information +- Reflect on how the improvement strategy aligns with current industry trends and emerging technologies +- Propose a mechanism for tracking the progress and impact of implemented improvements +- Suggest how this analysis process itself could be enhanced for future iterations + +Remember, your goal is to provide a transformative yet pragmatic roadmap that elevates the quality, performance, and maintainability of the codebase while considering the realities of the development team and business constraints. +--- +## The codebase: + + + + +## Table of Contents + +{% for file in files %}{{ file.path }} +{% endfor %} + + + +{% for file in files %} +## {{ file.path }} + +```{{ file.language }} +{{ file.content }} +``` + +{% endfor %} + + + diff --git a/code2prompt/templates/code-review.j2 b/code2prompt/templates/code-review.j2 new file mode 100644 index 0000000..fa5e05d --- /dev/null +++ b/code2prompt/templates/code-review.j2 @@ -0,0 +1,110 @@ +# Role: Expert Code Reviewer and Software Architect + +You are a world-class code reviewer and software architect with decades of experience across multiple programming languages and paradigms. Your expertise spans from low-level optimization to high-level architectural design. You have a keen eye for code quality, security, and scalability. + +# Task Overview + +Conduct a thorough code review of the provided files, focusing on improving code quality, robustness, simplicity, and documentation. Your review should be insightful, actionable, and prioritized. + +# Review Process + +1. Initial Assessment: + - Quickly scan all files to understand the overall structure and purpose of the code. + - Identify the primary programming paradigms and architectural patterns in use. + +2. Detailed Analysis: + - Examine each file in depth, considering the following aspects: + a. Code Quality + b. Robustness and Error Handling + c. Simplification and Refactoring + d. Naming and Documentation + e. Security and Best Practices + f. Performance and Scalability + +3. Prioritization: + - Categorize your findings into: + - Critical: Issues that could lead to bugs, security vulnerabilities, or significant performance problems. + - Important: Violations of best practices or areas for significant improvement. + - Minor: Style issues or small optimizations. + +4. Recommendations: + - For each issue, provide: + - A clear explanation of the problem + - The potential impact or risk + - A suggested solution or improvement + - Use the following format for each recommendation: + ``` + [Category: Critical/Important/Minor] + Issue: [Brief description] + Location: [File name and line number(s)] + Impact: [Potential consequences] + Recommendation: [Suggested fix or improvement] + Example: + Before: [Code snippet or description] + After: [Improved code snippet or description] + Rationale: [Explanation of the benefits of this change] + ``` + +5. Overall Assessment: + - Provide a high-level summary of the codebase's strengths and weaknesses. + - Suggest any architectural or structural changes that could benefit the project. + +6. Large Codebases: + - If reviewing a large codebase or multiple interconnected files, focus on: + a. Identifying common patterns or anti-patterns across files + b. Assessing overall architecture and suggesting improvements + c. Highlighting any inconsistencies in style or approach between different parts of the codebase + +7. Testing and Quality Assurance: + - Evaluate the existing test coverage (if any) + - Suggest areas where additional unit tests could be beneficial + - Recommend integration or end-to-end tests if appropriate + +8. Self-Reflection: + - Acknowledge any areas where the analysis might be limited due to lack of context or specific domain knowledge + - Suggest specific questions or areas where human developer input would be valuable + +# Guidelines + +- Preserve existing functionality unless explicitly improving error handling or security. +- Infer and respect the original code's intent. +- Focus on impactful improvements rather than nitpicking minor style issues. +- If any part of the original code is unclear, state your assumptions and request clarification. +- Consider the broader context and potential scalability of the code. + +# Output Format + +0. Initial Assessment and all your reflexions in +1. High-Priority Issues (Critical and Important findings) in tags +2. Other Recommendations (Minor issues and general improvements) under +3. Architectural Considerations (If applicable) in +4. Testing Recommendations in +5. Limitations and Further Inquiries in +6. Conclusion and Next Steps in + +Your review should be comprehensive, insightful, and actionable, providing clear value to the development team. + +--- +## The codebase: + + + + +## Table of Contents + +{% for file in files %}{{ file.path }} +{% endfor %} + + + +{% for file in files %} +## {{ file.path }} + +```{{ file.language }} +{{ file.content }} +``` + +{% endfor %} + + + diff --git a/code2prompt/templates/create-function.j2 b/code2prompt/templates/create-function.j2 new file mode 100644 index 0000000..a6bd364 --- /dev/null +++ b/code2prompt/templates/create-function.j2 @@ -0,0 +1,86 @@ + +# Write a function + +## Your Role + +You are an elite software developer with extensive. You have a strong background in debugging complex issues and optimizing code performance. + +## Task Overview + +You need provide a correct and tested implementation of the `{%input:function_name%}` + + +## Detailed Requirements + +{%input:function_description%} + +## Output Format + +Please provide your response in the following structured format: + +1. + Detailed specification of the is_filtered function based on the given information and your analysis. You must include the function signature, parameters, and expected behavior. + Use markdown. + + +2. + Provide a set of test cases that cover different scenarios for the is_filtered function. Include both positive and negative test cases to validate the implementation. + Propose edge case scenarios that might challenge the function's logic. + Use markdown code blocks to format the test cases. + + + +3. + + Unit test in markdown format, use code block to format the test + + ... other tests ... + + + +3. + Describe your initial implementation of the function, including any assumptions or design decisions you made. Explain how you approached the problem and any challenges you encountered. + Be careful to follow the specification and provide a clear explanation of your code. The function must pass the provided test cases. + Format as a code block in markdown + + +4. + Evaluate the strengths and weaknesses of your initial implementation. Discuss any limitations or areas for improvement in the code. + + +5. + Provide the complete, updated code for the function. Include all existing comments and add new comments where necessary to explain the changes and their purpose. + Format as a code block in markdown + + + +## Table of Contents + +{% for file in files %}{{ file.path }} +{% endfor %} + + + +{% for file in files %} +## {{ file.path }} + +```{{ file.language }} +{{ file.content }} +``` + +{% endfor %} + + + diff --git a/code2prompt/templates/create-readme.j2 b/code2prompt/templates/create-readme.j2 new file mode 100644 index 0000000..fa7ab9b --- /dev/null +++ b/code2prompt/templates/create-readme.j2 @@ -0,0 +1,128 @@ +## Role and Expertise: + +You are an elite technical writer and documentation specialist with deep expertise in growth hacking, open-source software, and GitHub best practices. Your mission is to craft an exceptional README.md file that will significantly boost a project's visibility, adoption, and community engagement. + +## Your Task: + +1. Analyze the provided code base meticulously, focusing on: + - Core functionality and unique selling points + - Technical architecture and design patterns + - Integration capabilities and extensibility + - Performance characteristics and scalability + - Security features and compliance standards (if applicable) + +2. Generate a comprehensive list of key components for an ideal README.md. Present these in a section, structured as a markdown checklist. + +3. Craft a stellar README.md file, presented in an section. This README should not only inform but inspire and engage potential users and contributors. + +## README.md Requirements: + +Your README.md must include: + +1. Project Title and Description + - Concise, compelling project summary + - Eye-catching logo or banner (placeholder if not provided) + +2. Badges + - Build status, version, license, code coverage, etc. + +3. Key Features + - Bulleted list of main functionalities and unique selling points + +4. Quick Start Guide + - Step-by-step installation instructions + - Basic usage example + +5. Detailed Documentation + - In-depth usage instructions + - API reference (if applicable) + - Configuration options + +6. Examples and Use Cases + - Code snippets demonstrating common scenarios + - Links to more extensive examples or demos + +7. Project Structure + - Brief overview of the repository's organization + +8. Dependencies + - List of required libraries, frameworks, and tools + - Compatibility information (OS, language versions, etc.) + +9. Contributing Guidelines + - How to submit issues, feature requests, and pull requests + - Coding standards and commit message conventions + +10. Testing + - Instructions for running tests + - Information on the testing framework used + +11. Deployment + - Guidelines for deploying the project (if applicable) + +12. Roadmap + - Future plans and upcoming features + +13. License + - Clear statement of the project's license + +14. Acknowledgments + - Credits to contributors, inspirations, or related projects + +15. Contact Information + - How to reach the maintainers + - Links to community channels (Slack, Discord, etc.) + +## Styling and Formatting: + +- Use clear, concise language optimized for skimming and quick comprehension +- Employ a friendly, professional tone that reflects the project's ethos +- Utilize Markdown features effectively: + - Hierarchical headings (H1 for title, H2 for main sections, H3 for subsections) + - Code blocks with appropriate language highlighting + - Tables for structured data + - Blockquotes for important notes or quotes + - Horizontal rules to separate major sections +- Include a table of contents for easy navigation +- Use emojis sparingly to add visual interest without overwhelming +- As expert Github expert use all the markdown Github flavor you can to make the README.md more appealing +- Use Github badges that make sense for the project + +## Output Format: + +Structure your response as follows: + + + [Checklist of key README components] + + + + [Full content of the README.md] + + +Remember to tailor the content, tone, and technical depth to the project's target audience, whether they are beginners, experienced developers, or a specific niche within the tech community. + +--- +## The codebase: + + + + +## Table of Contents + +{% for file in files %}{{ file.path }} +{% endfor %} + + + +{% for file in files %} +## {{ file.path }} + +```{{ file.language }} +{{ file.content }} +``` + +{% endfor %} + + + diff --git a/code2prompt/templates/default.j2 b/code2prompt/templates/default.j2 new file mode 100644 index 0000000..a9716b2 --- /dev/null +++ b/code2prompt/templates/default.j2 @@ -0,0 +1,17 @@ +# Code summary +{% for file in files %}- {{ file.path }} +{% endfor %} + +## Files + +{% for file in files %} +## {{ file.path }} + +- Language: {{ file.language }} +- Size: {{ file.size }} bytes +- Last modified: {{ file.modified }} + +```{{ file.language }} +{{ file.content }} +``` +{% endfor %} \ No newline at end of file diff --git a/code2prompt/templates/improve-this-prompt.j2 b/code2prompt/templates/improve-this-prompt.j2 new file mode 100644 index 0000000..942ad42 --- /dev/null +++ b/code2prompt/templates/improve-this-prompt.j2 @@ -0,0 +1,77 @@ +## Who you are +You are an elite prompt engineer with unparalleled expertise in crafting sophisticated and effective prompts. Your task is to significantly enhance the following prompt: + +## The Prompt: + + +{{input:prompt}} + + +### Your Task + +1. Analyze the input prompt, identifying its: + - Primary objective + - Target audience + - Key constraints or requirements + - Potential weaknesses or areas for improvement + +2. Apply at least two of the following advanced prompting techniques to enhance the prompt: + - Chain of Thought (CoT): Break down complex reasoning into steps. + - Tree of Thought (ToT): Explore multiple reasoning paths. + - Few-Shot Learning: Provide relevant examples. + - Role-Playing: Assume a specific persona or expertise. + - Metacognitive Prompting: Encourage self-reflection. + +3. Craft your improved prompt, ensuring it is: + - Clear and unambiguous + - Specific and detailed + - Designed to elicit high-quality, relevant responses + - Flexible enough to accommodate various scenarios + - Structured to maximize the AI's capabilities + +### Examples of Technique Application + +Chain of Thought (CoT): +"To answer this question, let's break it down into steps: +1. First, consider... +2. Next, analyze... +3. Finally, synthesize..." + +Role-Playing: +"Imagine you are a renowned expert in [field]. Given your extensive experience, how would you approach..." + +### Quality Metrics + +Evaluate your improved prompt based on: +1. Clarity: Is the prompt easy to understand? +2. Specificity: Does it provide clear guidelines and expectations? +3. Engagement: Does it inspire creative and thoughtful responses? +4. Versatility: Can it be applied to various scenarios within the context? +5. Depth: Does it encourage detailed and nuanced responses? + +### Iterative Refinement + +After crafting your initial improved prompt: +1. Critically review it against the quality metrics. +2. Identify at least one area for further improvement. +3. Refine the prompt based on this insight. +4. Repeat this process once more for optimal results. + +### Output Format + +Present your work in the following structure: + +1. Original Prompt Analysis in markdown format, in xml tags +2. First version Improved Prompt (in markdown format) in +3. Explanation of Applied Techniques in +4. Quality Metric Evaluation in +5. Iterative Refinement Process in +6. Final Thoughts on Improvement in +7. The final prompt in markdown format in + +By following this structured approach, you'll create a significantly enhanced prompt that drives high-quality AI outputs and addresses the specific needs of the given context. + + + + + diff --git a/code2prompt/utils/add_line_numbers.py b/code2prompt/utils/add_line_numbers.py new file mode 100644 index 0000000..ad16615 --- /dev/null +++ b/code2prompt/utils/add_line_numbers.py @@ -0,0 +1,15 @@ +def add_line_numbers(code: str) -> str: + """ + Adds line numbers to each line of the given code. + + Args: + code (str): The code to add line numbers to. + + Returns: + str: The code with line numbers added. + """ + lines = code.splitlines() + max_line_number = len(lines) + line_number_width = len(str(max_line_number)) + numbered_lines = [f"{i+1:{line_number_width}} | {line}" for i, line in enumerate(lines)] + return "\n".join(numbered_lines) \ No newline at end of file diff --git a/code2prompt/utils/analyzer.py b/code2prompt/utils/analyzer.py new file mode 100644 index 0000000..3c7b24d --- /dev/null +++ b/code2prompt/utils/analyzer.py @@ -0,0 +1,90 @@ +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Tuple + +def analyze_codebase(path: str) -> Tuple[Dict[str, int], Dict[str, List[str]]]: + """ + Analyze the codebase and return file extension information. + + Args: + path (str): The path to the codebase directory. + + Returns: + Tuple[Dict[str, int], Dict[str, List[str]]]: A tuple containing: + - A dictionary of file extensions and their counts. + - A dictionary of file extensions and the directories containing them. + """ + extension_counts = defaultdict(int) + extension_dirs = defaultdict(set) + + file_count = 0 + for file_path in Path(path).rglob('*'): + if file_path.is_file(): + file_count += 1 + ext = file_path.suffix.lower() + if ext: + extension_counts[ext] += 1 + extension_dirs[ext].add(str(file_path.parent)) + + if file_count == 0: + return {"No files found": 0}, {} + + return dict(extension_counts), {k: list(v) for k, v in extension_dirs.items()} + + +def format_flat_output(extension_counts: Dict[str, int]) -> str: + """ + Format the analysis results in a flat structure. + + Args: + extension_counts (Dict[str, int]): A dictionary of file extensions and their counts. + + Returns: + str: Formatted output string. + """ + output = [] + for ext, count in sorted(extension_counts.items()): + output.append(f"{ext}: {count} file{'s' if count > 1 else ''}") + return "\n".join(output) + +def format_tree_output(extension_dirs: Dict[str, List[str]]) -> str: + """ + Format the analysis results in a tree-like structure. + + Args: + extension_dirs (Dict[str, List[str]]): A dictionary of file extensions and their directories. + + Returns: + str: Formatted output string. + """ + def format_tree(node, prefix=""): + output = [] + for i, (key, value) in enumerate(node.items()): + is_last = i == len(node) - 1 + output.append(f"{prefix}{'└── ' if is_last else 'β”œβ”€β”€ '}{key}") + if isinstance(value, dict): + extension = " " if is_last else "β”‚ " + output.extend(format_tree(value, prefix + extension)) + return output + + tree = {} + for ext, dirs in extension_dirs.items(): + for dir_path in dirs: + current = tree + for part in Path(dir_path).parts: + current = current.setdefault(part, {}) + current[ext] = {} + + return "\n".join(format_tree(tree)) + +def get_extension_list(extension_counts: Dict[str, int]) -> str: + """ + Generate a comma-separated list of file extensions. + + Args: + extension_counts (Dict[str, int]): A dictionary of file extensions and their counts. + + Returns: + str: Comma-separated list of file extensions. + """ + return ",".join(sorted(extension_counts.keys())) \ No newline at end of file diff --git a/code2prompt/utils/config.py b/code2prompt/utils/config.py new file mode 100644 index 0000000..59356b2 --- /dev/null +++ b/code2prompt/utils/config.py @@ -0,0 +1,52 @@ +# code2prompt/config.py +import json +from pathlib import Path + +def load_config(current_dir): + """ + Load configuration from .code2promptrc files. + Searches in the current directory and all parent directories up to the home directory. + """ + config = {} + current_path = Path(current_dir).resolve() + home_path = Path.home() + while current_path >= home_path: + rc_file = current_path / '.code2promptrc' + if rc_file.is_file(): + with open(rc_file, 'r', encoding='utf-8') as f: + file_config = json.load(f) + if 'path' in file_config and isinstance(file_config['path'], str): + file_config['path'] = file_config['path'].split(',') + config.update(file_config) + if current_path == home_path: + break + current_path = current_path.parent + return config + +def merge_options(cli_options: dict, config_options: dict, default_options: dict) -> dict: + """ + Merge CLI options, config options, and default options. + CLI options take precedence over config options, which take precedence over default options. + """ + merged = default_options.copy() + + # Update with config options + for key, value in config_options.items(): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = merge_options({}, value, merged[key]) + else: + merged[key] = value + + # Update with CLI options, but only if they're different from the default + for key, value in cli_options.items(): + if value != default_options.get(key): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = merge_options(value, {}, merged[key]) + else: + merged[key] = value + + # Special handling for 'path' + if not merged['path'] and 'path' in config_options: + merged['path'] = config_options['path'] + + return merged \ No newline at end of file diff --git a/code2prompt/utils/count_tokens.py b/code2prompt/utils/count_tokens.py new file mode 100644 index 0000000..420d000 --- /dev/null +++ b/code2prompt/utils/count_tokens.py @@ -0,0 +1,21 @@ +import click +import tiktoken + + +def count_tokens(text: str, encoding: str) -> int: + """ + Count the number of tokens in the given text using the specified encoding. + + Args: + text (str): The text to tokenize and count. + encoding (str): The encoding to use for tokenization. + + Returns: + int: The number of tokens in the text. + """ + try: + encoder = tiktoken.get_encoding(encoding) + return len(encoder.encode(text)) + except Exception as e: + click.echo(f"Error counting tokens: {str(e)}", err=True) + return 0 \ No newline at end of file diff --git a/code2prompt/utils/create_template_directory.py b/code2prompt/utils/create_template_directory.py new file mode 100644 index 0000000..5a67264 --- /dev/null +++ b/code2prompt/utils/create_template_directory.py @@ -0,0 +1,92 @@ +import os +import shutil +from pathlib import Path +import logging +import tempfile + +logger = logging.getLogger(__name__) + +def create_templates_directory(package_templates_dir: Path, templates_dir: Path, dry_run=False, force=False, skip_existing=False): + """ + Create a 'templates' directory in the current working directory and populate it with template files + from the package's templates directory. + + Args: + package_templates_dir (Path): Path to the package's templates directory. + templates_dir (Path): Path to the directory where templates will be copied. + dry_run (bool): If True, show what changes would be made without making them. + force (bool): If True, overwrite existing files without prompting. + skip_existing (bool): If True, skip existing files without prompting or overwriting. + + Raises: + FileNotFoundError: If the package templates directory is not found. + PermissionError: If there's a permission issue creating directories or copying files. + IOError: If there's an IO error during the copy process. + """ + if not package_templates_dir.exists(): + logger.error(f"Package templates directory not found: {package_templates_dir}") + raise FileNotFoundError(f"Package templates directory not found: {package_templates_dir}") + + if dry_run: + logger.info("Dry run mode: No changes will be made.") + + try: + if not dry_run: + templates_dir.mkdir(exist_ok=True, parents=True) + if not os.access(templates_dir, os.W_OK): + raise PermissionError(f"No write permission for directory: {templates_dir}") + logger.info(f"Templates directory {'would be' if dry_run else 'was'} created at: {templates_dir}") + except PermissionError as e: + logger.error(f"Permission error: {str(e)}") + raise + + # Check available disk space only if not in dry run mode + if not dry_run: + try: + _, _, free = shutil.disk_usage(templates_dir) + required_space = sum(f.stat().st_size for f in package_templates_dir.glob('**/*') if f.is_file()) + if free < required_space: + raise IOError(f"Insufficient disk space. Required: {required_space}, Available: {free}") + except OSError as e: + logger.error(f"Error checking disk space: {str(e)}") + raise + + copied_files = [] + try: + for template_file in package_templates_dir.iterdir(): + if template_file.is_file(): + dest_file = templates_dir / template_file.name + if dest_file.exists(): + if skip_existing: + logger.info(f"Skipping existing file: {dest_file}") + continue + if not force: + if dry_run: + logger.info(f"Would prompt to overwrite: {dest_file}") + continue + overwrite = input(f"{dest_file} already exists. Overwrite? (y/n): ").lower() == 'y' + if not overwrite: + logger.info(f"Skipping: {template_file.name}") + continue + + try: + if not dry_run: + # Use a temporary file to ensure atomic write + with tempfile.NamedTemporaryFile(dir=templates_dir, delete=False) as tmp_file: + shutil.copy2(template_file, tmp_file.name) + os.replace(tmp_file.name, dest_file) + copied_files.append(dest_file) + logger.info(f"Template {'would be' if dry_run else 'was'} copied: {template_file.name}") + except (PermissionError, IOError) as e: + logger.error(f"Error copying {template_file.name}: {str(e)}") + raise + + except Exception as e: + logger.error(f"An error occurred during the template creation process: {str(e)}") + if not dry_run: + # Clean up partially copied files + for file in copied_files: + file.unlink(missing_ok=True) + raise + + logger.info("Template creation process completed.") diff --git a/code2prompt/utils/display_price_table.py b/code2prompt/utils/display_price_table.py new file mode 100644 index 0000000..847ca83 --- /dev/null +++ b/code2prompt/utils/display_price_table.py @@ -0,0 +1,115 @@ +from code2prompt.utils.price_calculator import calculate_prices, load_token_prices + +import click +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich.text import Text + + +def format_price(price: float, is_total: bool = False) -> str: + """ + Formats the given price as a string. + + Args: + price (float): The price to be formatted. + is_total (bool, optional): Indicates whether the price is a total. Defaults to False. + + Returns: + str: The formatted price as a string. + + """ + if is_total: + return f"${price:.6f}" + return f"${price /1_000 * 1_000_000 :.2f}" + + +def format_specific_price(price: float, tokens: int) -> str: + """ + Formats the specific price based on the given price and tokens. + + Args: + price (float): The price value. + tokens (int): The number of tokens. + + Returns: + str: The formatted specific price. + + """ + return f"${(price * tokens / 1_000):.6f}" + + +def display_price_table( + output_tokens: int, provider: str, model: str, token_count: int +): + """ + Displays a price table with estimated token prices based on the token count and provider's pricing model. + + Args: + output_tokens (int): The number of output tokens. + provider (str): The name of the provider. + model (str): The name of the model. + token_count (int): The number of input tokens. + + Returns: + None + """ + token_prices = load_token_prices() + if not token_prices: + return + price_results = calculate_prices( + token_prices, token_count, output_tokens, provider, model + ) + + if not price_results: + click.echo("Error: No matching provider or model found") + return + + console = Console() + + table = Table(show_header=True, header_style="bold magenta", expand=True) + table.add_column("Provider", style="cyan", no_wrap=True) + table.add_column("Model", style="green") + table.add_column("Input Price\n($/1M tokens)", justify="right", style="yellow") + table.add_column("Output Price\n($/1M tokens)", justify="right", style="yellow") + table.add_column("Tokens\nOut | In", justify="right", style="blue") + table.add_column("Price $\nOut | In", justify="right", style="magenta") + table.add_column("Total Cost", justify="right", style="red") + + for result in price_results: + input_price = format_price(result.price_input) + output_price = format_price(result.price_output) + specific_input_price = format_specific_price(result.price_input, token_count) + specific_output_price = format_specific_price( + result.price_output, output_tokens + ) + total_price = format_price(result.total_price, is_total=True) + + table.add_row( + result.provider_name, + result.model_name, + input_price, + output_price, + f"{token_count:,} | {output_tokens:,}", + f"{specific_input_price} | {specific_output_price}", + total_price, + ) + + title = Text("Estimated Token Prices", style="bold white on blue") + subtitle = Text("All prices in USD", style="italic") + + panel = Panel( + table, title=title, subtitle=subtitle, expand=False, border_style="blue" + ) + + console.print("\n") + console.print(panel) + console.print( + "\nπŸ“Š Note: Prices are based on the token count and provider's pricing model." + ) + console.print( + "πŸ’‘ Tip: 'Price $ In | Out' shows the cost for the specific input and output tokens." + ) + console.print( + "⚠️ This is an estimate based on OpenAI's Tokenizer implementation.\n" + ) diff --git a/code2prompt/utils/file_utils.py b/code2prompt/utils/file_utils.py new file mode 100644 index 0000000..aa1cbae --- /dev/null +++ b/code2prompt/utils/file_utils.py @@ -0,0 +1,134 @@ +# code2prompt/utils/file_utils.py + +from pathlib import Path +from typing import List, Dict, Any +import logging + +from code2prompt.config import Configuration +from code2prompt.utils.is_binary import is_binary +from code2prompt.utils.is_filtered import is_filtered +from code2prompt.utils.is_ignored import is_ignored +from code2prompt.utils.get_gitignore_patterns import get_gitignore_patterns +from code2prompt.core.process_file import process_file + +logger = logging.getLogger(__name__) + +def process_files(config: Configuration) -> List[Dict[str, Any]]: + """ + Process files based on the provided configuration. + + Args: + config (Configuration): Configuration object containing processing options. + + Returns: + List[Dict[str, Any]]: A list of dictionaries containing processed file data. + """ + files_data = [] + for path in config.path: + path = Path(path) + gitignore_patterns = get_gitignore_patterns( + path.parent if path.is_file() else path, + config.gitignore + ) + + if path.is_file(): + file_data = process_single_file(path, gitignore_patterns, config) + if file_data: + files_data.append(file_data) + else: + files_data.extend(process_directory(path, gitignore_patterns, config)) + + return files_data + +def process_single_file( + file_path: Path, + gitignore_patterns: List[str], + config: Configuration +) -> Dict[str, Any]: + """ + Process a single file if it meets the criteria. + + Args: + file_path (Path): Path to the file to process. + gitignore_patterns (List[str]): List of gitignore patterns. + config (Configuration): Configuration object containing processing options. + + Returns: + Dict[str, Any]: Processed file data if the file should be processed, None otherwise. + """ + if should_process_file(file_path, gitignore_patterns, file_path.parent, config): + return process_file( + file_path, + config.suppress_comments, + config.line_number, + config.no_codeblock + ) + return None + +def process_directory( + directory_path: Path, + gitignore_patterns: List[str], + config: Configuration +) -> List[Dict[str, Any]]: + """ + Process all files in a directory that meet the criteria. + + Args: + directory_path (Path): Path to the directory to process. + gitignore_patterns (List[str]): List of gitignore patterns. + config (Configuration): Configuration object containing processing options. + + Returns: + List[Dict[str, Any]]: List of processed file data for files that meet the criteria. + """ + files_data = [] + for file_path in directory_path.rglob("*"): + if file_path.is_file(): + file_data = process_single_file(file_path, gitignore_patterns, config) + if file_data: + files_data.append(file_data) + return files_data + +def should_process_file( + file_path: Path, + gitignore_patterns: List[str], + root_path: Path, + config: Configuration +) -> bool: + """ + Determine whether a file should be processed based on several criteria. + + Args: + file_path (Path): Path to the file to check. + gitignore_patterns (List[str]): List of gitignore patterns. + root_path (Path): Root path for relative path calculations. + config (Configuration): Configuration object containing processing options. + + Returns: + bool: True if the file should be processed, False otherwise. + """ + logger.debug(f"Checking if should process file: {file_path}") + + if not file_path.is_file(): + logger.debug(f"Skipping {file_path}: Not a file.") + return False + + if is_ignored(file_path, gitignore_patterns, root_path): + logger.debug(f"Skipping {file_path}: File is ignored based on gitignore patterns.") + return False + + if not is_filtered( + file_path, + config.filter, + config.exclude, + config.case_sensitive + ): + logger.debug(f"Skipping {file_path}: File does not meet filter criteria.") + return False + + if is_binary(file_path): + logger.debug(f"Skipping {file_path}: File is binary.") + return False + + logger.debug(f"Processing file: {file_path}") + return True \ No newline at end of file diff --git a/code2prompt/utils/generate_markdown_content.py b/code2prompt/utils/generate_markdown_content.py new file mode 100644 index 0000000..0244da5 --- /dev/null +++ b/code2prompt/utils/generate_markdown_content.py @@ -0,0 +1,37 @@ +def generate_markdown_content(files_data, no_codeblock): + """ + Generates a Markdown content string from the provided files data. + + Parameters: + - files_data (list of dict): A list of dictionaries containing file information and content. + - no_codeblock (bool): Flag indicating whether to disable wrapping code inside markdown code blocks. + + Returns: + - str: A Markdown-formatted string containing the table of contents and the file contents. + """ + table_of_contents = [f"- {file['path']}\n" for file in files_data] + + content = [] + for file in files_data: + file_info = ( + f"## File: {file['path']}\n\n" + f"- Extension: {file['extension']}\n" + f"- Language: {file['language']}\n" + f"- Size: {file['size']} bytes\n" + f"- Created: {file['created']}\n" + f"- Modified: {file['modified']}\n\n" + ) + + if no_codeblock: + file_code = f"### Code\n\n{file['content']}\n\n" + else: + file_code = f"### Code\n\n```{file['language']}\n{file['content']}\n```\n\n" + + content.append(file_info + file_code) + + return ( + "# Table of Contents\n" + + "".join(table_of_contents) + + "\n" + + "".join(content) + ) diff --git a/code2prompt/utils/get_gitignore_patterns.py b/code2prompt/utils/get_gitignore_patterns.py new file mode 100644 index 0000000..59d2809 --- /dev/null +++ b/code2prompt/utils/get_gitignore_patterns.py @@ -0,0 +1,27 @@ +from code2prompt.utils.parse_gitignore import parse_gitignore +from pathlib import Path + +def get_gitignore_patterns(path, gitignore): + """ + Retrieve gitignore patterns from a specified path or a default .gitignore file. + + This function reads the .gitignore file located at the specified path or uses + the default .gitignore file in the project root if no specific path is provided. + It then parses the file to extract ignore patterns and adds a default pattern + to ignore the .git directory itself. + + Args: + path (Path): The root path of the project where the default .gitignore file is located. + gitignore (Optional[str]): An optional path to a specific .gitignore file to use instead of the default. + + Returns: + Set[str]: A set of gitignore patterns extracted from the .gitignore file. + """ + if gitignore: + gitignore_path = Path(gitignore) + else: + gitignore_path = Path(path) / ".gitignore" + + patterns = parse_gitignore(gitignore_path) + patterns.add(".git") + return patterns \ No newline at end of file diff --git a/code2prompt/utils/include_loader.py b/code2prompt/utils/include_loader.py new file mode 100644 index 0000000..054a9a4 --- /dev/null +++ b/code2prompt/utils/include_loader.py @@ -0,0 +1,79 @@ +import os +from typing import List, Tuple, Callable +from jinja2 import BaseLoader, TemplateNotFound +import threading +from contextlib import contextmanager +import jinja2 # Import jinja2 to resolve the undefined name error + + +class CircularIncludeError(Exception): + """Exception raised when a circular include is detected in templates.""" + + pass + + +class IncludeLoader(BaseLoader): + """ + A custom Jinja2 loader that supports file inclusion with circular dependency detection. + + This loader keeps track of the include stack for each thread to prevent circular includes. + It raises a CircularIncludeError if a circular include is detected. + + Attributes: + path (str): The base path for template files. + encoding (str): The encoding to use when reading template files. + include_stack (threading.local): Thread-local storage for the include stack. + """ + + def __init__(self, path: str, encoding: str = "utf-8"): + """ + Initialize the IncludeLoader. + + Args: + path (str): The base path for template files. + encoding (str, optional): The encoding to use when reading template files. Defaults to 'utf-8'. + """ + self.path: str = path + self.encoding: str = encoding + self.include_stack: threading.local = threading.local() + + @contextmanager + def _include_stack_context(self, path): + if not hasattr(self.include_stack, "stack"): + self.include_stack.stack = set() + if path in self.include_stack.stack: + raise CircularIncludeError(f"Circular include detected: {path}") + self.include_stack.stack.add(path) + try: + yield + finally: + self.include_stack.stack.remove(path) + + def get_source( + self, environment: "jinja2.Environment", template: str + ) -> Tuple[str, str, Callable[[], bool]]: + path: str = os.path.join(self.path, template) + if not os.path.exists(path): + raise TemplateNotFound(f"{template} (searched in {self.path})") + + with self._include_stack_context(path): + try: + with open(path, "r", encoding=self.encoding) as f: + source: str = f.read() + except IOError as e: + raise TemplateNotFound( + template, message=f"Error reading template file: {e}" + ) from e + + return source, path, lambda: True + + def list_templates(self) -> List[str]: + """ + List all available templates. + + This method is not implemented for this loader and always returns an empty list. + + Returns: + List[str]: An empty list. + """ + return [] diff --git a/code2prompt/utils/is_binary.py b/code2prompt/utils/is_binary.py new file mode 100644 index 0000000..82de52b --- /dev/null +++ b/code2prompt/utils/is_binary.py @@ -0,0 +1,8 @@ +def is_binary(file_path): + try: + with open(file_path, "rb") as file: + chunk = file.read(1024) + return b"\x00" in chunk + except IOError: + print(f"Error: The file at {file_path} could not be opened.") + return False \ No newline at end of file diff --git a/code2prompt/utils/is_filtered.py b/code2prompt/utils/is_filtered.py new file mode 100644 index 0000000..a4d5ce1 --- /dev/null +++ b/code2prompt/utils/is_filtered.py @@ -0,0 +1,69 @@ +""" +This module contains utility functions for filtering files based on include and exclude patterns. +""" + +from pathlib import Path +from fnmatch import fnmatch + + +def is_filtered( + file_path: Path, + include_pattern: str = "", + exclude_pattern: str = "", + case_sensitive: bool = False, +) -> bool: + """ + Determine if a file should be filtered based on include and exclude patterns. + + Parameters: + - file_path (Path): Path to the file to check + - include_pattern (str): Comma-separated list of patterns to include files + - exclude_pattern (str): Comma-separated list of patterns to exclude files + - case_sensitive (bool): Whether to perform case-sensitive pattern matching + + Returns: + - bool: True if the file should be included, False if it should be filtered out + """ + + def match_pattern(path: str, pattern: str) -> bool: + if "**" in pattern: + parts = pattern.split("**") + return any(fnmatch(path, f"*{p}*") for p in parts if p) + return fnmatch(path, pattern) + + def match_patterns(path: str, patterns: list) -> bool: + return any(match_pattern(path, pattern) for pattern in patterns) + + # Convert file_path to string + file_path_str = str(file_path) + + # Handle case sensitivity + if not case_sensitive: + file_path_str = file_path_str.lower() + + # Prepare patterns + def prepare_patterns(pattern): + if isinstance(pattern, str): + return [p.strip().lower() for p in pattern.split(",") if p.strip()] + elif isinstance(pattern, (list, tuple)): + return [str(p).strip().lower() for p in pattern if str(p).strip()] + else: + return [] + + include_patterns = prepare_patterns(include_pattern) + exclude_patterns = prepare_patterns(exclude_pattern) + + # If no patterns are specified, include the file + if not include_patterns and not exclude_patterns: + return True + + # Check exclude patterns first (they take precedence) + if match_patterns(file_path_str, exclude_patterns): + return False # Exclude dotfiles and other specified patterns + + # If include patterns are specified, the file must match at least one + if include_patterns: + return match_patterns(file_path_str, include_patterns) + + # If we reach here, there were no include patterns and the file wasn't excluded + return True diff --git a/code2prompt/utils/is_ignored.py b/code2prompt/utils/is_ignored.py new file mode 100644 index 0000000..6fb1706 --- /dev/null +++ b/code2prompt/utils/is_ignored.py @@ -0,0 +1,34 @@ +from fnmatch import fnmatch +from pathlib import Path + + + +def is_ignored(file_path: Path, gitignore_patterns: list, base_path: Path) -> bool: + """ + Check if a file is ignored based on gitignore patterns. + + Args: + file_path (Path): The path of the file to check. + gitignore_patterns (list): List of gitignore patterns. + base_path (Path): The base path to resolve relative paths. + + Returns: + bool: True if the file is ignored, False otherwise. + """ + relative_path = file_path.relative_to(base_path) + for pattern in gitignore_patterns: + pattern = pattern.rstrip("/") + if pattern.startswith("/"): + if fnmatch(str(relative_path), pattern[1:]): + return True + if fnmatch(str(relative_path.parent), pattern[1:]): + return True + else: + for path in relative_path.parents: + if fnmatch(str(path / relative_path.name), pattern): + return True + if fnmatch(str(path), pattern): + return True + if fnmatch(str(relative_path), pattern): + return True + return False \ No newline at end of file diff --git a/code2prompt/utils/language_inference.py b/code2prompt/utils/language_inference.py new file mode 100644 index 0000000..7ee5375 --- /dev/null +++ b/code2prompt/utils/language_inference.py @@ -0,0 +1,129 @@ +import os + + +def infer_language(filename: str, syntax_map: dict) -> str: + """ + Infers the programming language of a given file based on its extension. + + Parameters: + - filename (str): The name of the file including its extension. + - syntax_map (dict): Custom syntax mappings for language inference. + + Returns: + - str: The inferred programming language as a lowercase string, e.g., "python". + Returns "unknown" if the language cannot be determined. + """ + _, extension = os.path.splitext(filename) + extension = extension.lower() + + # Check user-defined syntax map first + if extension in syntax_map: + return syntax_map[extension] + + language_map = { + ".c": "c", + ".h": "c", + ".cpp": "cpp", + ".hpp": "cpp", + ".cc": "cpp", + ".cxx": "cpp", + ".java": "java", + ".js": "javascript", + ".jsx": "javascript", + ".ts": "typescript", + ".tsx": "typescript", + ".cs": "csharp", + ".php": "php", + ".go": "go", + ".rs": "rust", + ".kt": "kotlin", + ".swift": "swift", + ".scala": "scala", + ".dart": "dart", + ".py": "python", + ".rb": "ruby", + ".pl": "perl", + ".pm": "perl", + ".sh": "bash", + ".bash": "bash", + ".zsh": "zsh", + ".ps1": "powershell", + ".html": "html", + ".htm": "html", + ".xml": "xml", + ".sql": "sql", + ".m": "matlab", + ".r": "r", + ".lua": "lua", + ".jl": "julia", + ".f": "fortran", + ".f90": "fortran", + ".hs": "haskell", + ".lhs": "haskell", + ".ml": "ocaml", + ".erl": "erlang", + ".ex": "elixir", + ".exs": "elixir", + ".clj": "clojure", + ".coffee": "coffeescript", + ".groovy": "groovy", + ".pas": "pascal", + ".vb": "visualbasic", + ".asm": "assembly", + ".s": "assembly", + ".lisp": "lisp", + ".cl": "lisp", + ".scm": "scheme", + ".rkt": "racket", + ".fs": "fsharp", + ".d": "d", + ".ada": "ada", + ".nim": "nim", + ".cr": "crystal", + ".v": "verilog", + ".vhd": "vhdl", + ".tcl": "tcl", + ".elm": "elm", + ".zig": "zig", + ".raku": "raku", + ".perl6": "raku", + ".p6": "raku", + ".vim": "vimscript", + ".ps": "postscript", + ".prolog": "prolog", + ".cobol": "cobol", + ".cob": "cobol", + ".cbl": "cobol", + ".forth": "forth", + ".fth": "forth", + ".abap": "abap", + ".apex": "apex", + ".sol": "solidity", + ".hack": "hack", + ".sml": "standardml", + ".purs": "purescript", + ".idr": "idris", + ".agda": "agda", + ".lean": "lean", + ".wasm": "webassembly", + ".wat": "webassembly", + ".j2": "jinja2", + ".md": "markdown", + ".tex": "latex", + ".bib": "bibtex", + ".yaml": "yaml", + ".yml": "yaml", + ".json": "json", + ".toml": "toml", + ".ini": "ini", + ".cfg": "ini", + ".conf": "ini", + ".dockerfile": "dockerfile", + ".docker": "dockerfile", + '.txt': 'plaintext', + '.csv': 'csv', + '.tsv': 'tsv', + '.log': 'log' + } + + return language_map.get(extension, "unknown") diff --git a/code2prompt/utils/logging_utils.py b/code2prompt/utils/logging_utils.py new file mode 100644 index 0000000..f78861f --- /dev/null +++ b/code2prompt/utils/logging_utils.py @@ -0,0 +1,65 @@ +import logging +import colorlog +import sys + +def setup_logger(level="INFO"): + """Set up the logger with the specified logging level.""" + logger = colorlog.getLogger() + logger.setLevel(level) + + # Create console handler + ch = colorlog.StreamHandler() + ch.setLevel(level) + + # Create formatter with a more structured format + formatter = colorlog.ColoredFormatter( + '%(log_color)s[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + ch.setFormatter(formatter) + + # Add the handler to the logger + logger.addHandler(ch) + + return logger + +def log_error(message): + """Log an error message.""" + logger = logging.getLogger() + logger.error(message) + +def log_output_created(output_path): + """Log a message indicating that an output file has been created.""" + logger = logging.getLogger() + logger.info(f"Output file created: {output_path}") + +def log_clipboard_copy(success): + """Log a message indicating whether copying to clipboard was successful.""" + logger = logging.getLogger() + if success: + success_message = "\033[92mπŸ“‹ Content copied to clipboard successfully.\033[0m" + logger.info(success_message) + print(success_message, file=sys.stderr) + else: + logger.error("Failed to copy content to clipboard.") + print("Failed to copy content to clipboard.", file=sys.stderr) + +def log_token_count(token_count): + """Log the token count.""" + # Calculate the number of tokens in the input + token_count_message = f"\n✨ \033[94mToken count: {token_count}\033[0m\n" # Added color for better display + print(token_count_message, file=sys.stderr) + +def log_token_prices(prices): + """Log the estimated token prices.""" + # Remove the unused logger variable + # logger = logging.getLogger() # Unused variable + header = "─────────────────────────────────────────────────── Estimated Token Prices ───────────────────────────────────────────────────" + print(header) + print("┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓") + print("┃ ┃ ┃ Input Price ┃ Output Price ┃ Tokens ┃ Price $ ┃ ┃") + print("┃ Provider ┃ Model ┃ ($/1M tokens) ┃ ($/1M tokens) ┃ In | Out ┃ In | Out ┃ Total Cost ┃") + print("┑━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩") + for price in prices: + print(f"β”‚ {price['provider']: <11} β”‚ {price['model']: <19} β”‚ {price['input_price']: >13} β”‚ {price['output_price']: >13} β”‚ {price['tokens_in']: >13} | {price['tokens_out']: >13} β”‚ {price['input_cost']: >12} | {price['output_cost']: >12} β”‚ {price['total_cost']: >12} β”‚") + print("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜") diff --git a/code2prompt/utils/output_utils.py b/code2prompt/utils/output_utils.py new file mode 100644 index 0000000..4216493 --- /dev/null +++ b/code2prompt/utils/output_utils.py @@ -0,0 +1,127 @@ +# code2prompt/utils/output_utils.py + +from pathlib import Path +import logging +from typing import Dict, List, Optional + +from rich import print as rprint +from rich.panel import Panel +from rich.syntax import Syntax + +from code2prompt.config import Configuration + + +def generate_content(files_data: List[Dict], config: Configuration) -> str: + """ + Generate content based on the provided files data and configuration. + + Args: + files_data (List[Dict]): A list of dictionaries containing processed file data. + config (Configuration): Configuration object containing options. + + Returns: + str: The generated content as a string. + """ + if config.template: + return _process_template(files_data, config) + return _generate_markdown_content(files_data, config.no_codeblock) + + +def _process_template(files_data: List[Dict], config: Configuration) -> str: + """ + Process a Jinja2 template with the given files data and user inputs. + + Args: + files_data (List[Dict]): A list of dictionaries containing processed file data. + config (Configuration): Configuration object containing options. + + Returns: + str: The processed template content. + """ + from code2prompt.core.template_processor import ( + get_user_inputs, + load_template, + process_template, + ) + + template_content = load_template(config.template) + user_inputs = get_user_inputs(template_content) + return process_template(template_content, files_data, user_inputs, config.template) + + +def _generate_markdown_content(files_data: List[Dict], no_codeblock: bool) -> str: + """ + Generate markdown content from the provided files data. + + Args: + files_data (List[Dict]): A list of dictionaries containing file information and content. + no_codeblock (bool): Flag indicating whether to disable wrapping code inside markdown code blocks. + + Returns: + str: A Markdown-formatted string containing the table of contents and the file contents. + """ + table_of_contents = [f"- {file['path']}\n" for file in files_data] + content = [] + + for file in files_data: + file_info = ( + f"## File: {file['path']}\n\n" + f"- Extension: {file['extension']}\n" + f"- Language: {file['language']}\n" + f"- Size: {file['size']} bytes\n" + f"- Created: {file['created']}\n" + f"- Modified: {file['modified']}\n\n" + ) + + if no_codeblock: + file_code = f"### Code\n\n{file['content']}\n\n" + else: + file_code = f"### Code\n\n```{file['language']}\n{file['content']}\n```\n\n" + + content.append(file_info + file_code) + + return "# Table of Contents\n" + "".join(table_of_contents) + "\n" + "".join(content) + + +def write_output(content: str, output_path: Optional[Path], logger: logging.Logger): + """ + Write the generated content to a file or print it to the console. + + Args: + content (str): The content to be written or printed. + output_path (Optional[Path]): The path to the file where the content should be written. + If None, the content is printed to the console. + logger (logging.Logger): Logger instance for logging messages. + """ + if output_path: + try: + with output_path.open("w", encoding="utf-8") as output_file: + output_file.write(content) + logger.info(f"Output file created: {output_path}") + except IOError as e: + logger.error(f"Error writing to output file: {e}") + else: + rprint(Panel(Syntax(content, "markdown", theme="monokai", line_numbers=True))) + + +def log_token_count(count: int): + """ + Log the total number of tokens processed. + + Args: + count (int): The total number of tokens processed. + """ + rprint(f"[cyan]πŸ”’ Token count: {count}[/cyan]") + + +def log_clipboard_copy(success: bool = True): + """ + Log whether the content was successfully copied to the clipboard. + + Args: + success (bool): Indicates whether the content was successfully copied to the clipboard. + """ + if success: + rprint("[green]πŸ“‹ Content copied to clipboard[/green]") + else: + rprint("[yellow]πŸ“‹ Failed to copy content to clipboard[/yellow]") \ No newline at end of file diff --git a/code2prompt/utils/parse_gitignore.py b/code2prompt/utils/parse_gitignore.py new file mode 100644 index 0000000..9a450a4 --- /dev/null +++ b/code2prompt/utils/parse_gitignore.py @@ -0,0 +1,6 @@ +def parse_gitignore(gitignore_path): + if not gitignore_path.exists(): + return set() + with gitignore_path.open("r", encoding="utf-8") as file: + patterns = set(line.strip() for line in file if line.strip() and not line.startswith("#")) + return patterns \ No newline at end of file diff --git a/code2prompt/utils/price_calculator.py b/code2prompt/utils/price_calculator.py new file mode 100644 index 0000000..15afbaa --- /dev/null +++ b/code2prompt/utils/price_calculator.py @@ -0,0 +1,150 @@ +import json +from typing import List, Optional +from pathlib import Path +from functools import lru_cache +from pydantic import BaseModel, ConfigDict, field_validator + + +class PriceModel(BaseModel): + price: Optional[float] = None + input_price: Optional[float] = None + output_price: Optional[float] = None + name: str + + @field_validator("price", "input_price", "output_price") + @classmethod + def check_price(cls, v: Optional[float]) -> Optional[float]: + if v is not None and v < 0: + raise ValueError("Price must be non-negative") + return v + + +class Provider(BaseModel): + name: str + models: List[PriceModel] + + +class TokenPrices(BaseModel): + providers: List[Provider] + + +class PriceResult(BaseModel): + provider_name: str + model_name: str + price_input: float + price_output: float + total_tokens: int + total_price: float + + model_config = ConfigDict(protected_namespaces=()) + + + + +@lru_cache(maxsize=1) +def load_token_prices() -> TokenPrices: + """ + Load token prices from a JSON file. + + Returns: + TokenPrices: A Pydantic model containing token prices. + + Raises: + RuntimeError: If there is an error loading the token prices. + """ + price_file = Path(__file__).parent.parent / "data" / "token_price.json" + try: + with price_file.open("r", encoding="utf-8") as f: + data = json.load(f) + return TokenPrices.model_validate(data) + except (IOError, json.JSONDecodeError) as e: + raise RuntimeError(f"Error loading token prices: {str(e)}") from e + + +def calculate_price(token_count: int, price_per_1000: float) -> float: + """ + Calculates the price based on the token count and price per 1000 tokens. + + Args: + token_count (int): The total number of tokens. + price_per_1000 (float): The price per 1000 tokens. + + Returns: + float: The calculated price. + """ + return (token_count / 1_000) * price_per_1000 + + +def calculate_prices( + token_prices: TokenPrices, + input_tokens: int, + output_tokens: int, + provider: Optional[str] = None, + model: Optional[str] = None, +) -> List[PriceResult]: + """ + Calculate the prices for a given number of input and output tokens based on token prices. + + Args: + token_prices (TokenPrices): A Pydantic model containing token prices for different providers and models. + input_tokens (int): The number of input tokens. + output_tokens (int): The number of output tokens. + provider (str, optional): The name of the provider. If specified, only prices for the specified provider will be calculated. + model (str, optional): The name of the model. If specified, only prices for the specified model will be calculated. + + Returns: + List[PriceResult]: A list of PriceResult objects containing the calculation results. + """ + results = [] + total_tokens = input_tokens + output_tokens + + for provider_data in token_prices.providers: + if provider and provider_data.name.lower() != provider.lower(): + continue + + for model_data in provider_data.models: + if model and model_data.name.lower() != model.lower(): + continue + + if model_data.price is not None: + price_input = model_data.price + price_output = model_data.price + total_price = calculate_price(total_tokens, model_data.price) + elif ( + model_data.input_price is not None + and model_data.output_price is not None + ): + price_input = model_data.input_price + price_output = model_data.output_price + total_price = calculate_price( + input_tokens, price_input + ) + calculate_price(output_tokens, price_output) + else: + continue + + results.append( + PriceResult( + provider_name=provider_data.name, + model_name=model_data.name, + price_input=price_input, + price_output=price_output, + total_tokens=total_tokens, + total_price=total_price, + ) + ) + + return results + + +if __name__ == "__main__": + # Example usage + token_prices = load_token_prices() + results = calculate_prices(token_prices, input_tokens=100, output_tokens=50) + for result in results: + print(f"Provider: {result.provider_name}") + print(f"Model: {result.model_name}") + print(f"Input Price: ${result.price_input:.10f}") + print(f"Output Price: ${result.price_output:.10f}") + print(f"Total Tokens: {result.total_tokens}") + print(f"Total Price: ${result.total_price:.10f}") + print("---") \ No newline at end of file diff --git a/code2prompt/utils/should_process_file.py b/code2prompt/utils/should_process_file.py new file mode 100644 index 0000000..0202f5e --- /dev/null +++ b/code2prompt/utils/should_process_file.py @@ -0,0 +1,59 @@ +""" + +This module contains the function to determine +if a file should be processed based on several criteria. + +""" + +import logging +from pathlib import Path +from typing import List # Add this import +from code2prompt.utils.is_binary import is_binary +from code2prompt.utils.is_filtered import is_filtered +from code2prompt.utils.is_ignored import is_ignored + +logger = logging.getLogger(__name__) + + +def should_process_file( + file_path: Path, + gitignore_patterns: List[str], # List is now defined + root_path: Path, + filter_patterns: str, ## comma separated list of patterns + exclude_patterns: str, ## comma separated list of patterns + case_sensitive: bool, +) -> bool: + """ + Determine whether a file should be processed based on several criteria. + """ + logger.debug( + "Checking if should process file: %s", file_path + ) # Use lazy % formatting + + if not file_path.is_file(): + logger.debug("Skipping %s: Not a file.", file_path) # Use lazy % formatting + return False + + if is_ignored(file_path, gitignore_patterns, root_path): + logger.debug( + "Skipping %s: File is ignored based on gitignore patterns.", file_path + ) + return False + + if not is_filtered( + file_path=file_path, + include_pattern=filter_patterns, + exclude_pattern=exclude_patterns, + case_sensitive=case_sensitive, + ): + logger.debug( + "Skipping %s: File does not meet filter criteria.", file_path + ) # Use lazy % formatting + return False + + if is_binary(file_path): + logger.debug("Skipping %s: File is binary.", file_path) # Use lazy % formatting + return False + + logger.debug("Processing file: %s", file_path) # Use lazy % formatting + return True diff --git a/code2prompt/version.py b/code2prompt/version.py new file mode 100644 index 0000000..d758dcf --- /dev/null +++ b/code2prompt/version.py @@ -0,0 +1 @@ +VERSION="0.9.0" \ No newline at end of file diff --git a/docs/demo01/doc01.md b/docs/demo01/doc01.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/demo01/doc02.md b/docs/demo01/doc02.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/screen-example1.png b/docs/screen-example1.png new file mode 100644 index 0000000..e3ba2b0 Binary files /dev/null and b/docs/screen-example1.png differ diff --git a/docs/screen-example2.png b/docs/screen-example2.png new file mode 100644 index 0000000..29cdf2a Binary files /dev/null and b/docs/screen-example2.png differ diff --git a/docs/screen-example3.png b/docs/screen-example3.png new file mode 100644 index 0000000..bd15fb1 Binary files /dev/null and b/docs/screen-example3.png differ diff --git a/poetry.lock b/poetry.lock index fd7fbfb..a4dec2b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,262 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "astroid" +version = "3.3.5" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, + {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +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.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] [[package]] name = "click" @@ -25,20 +283,166 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "colorlog" +version = "6.9.0" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, + {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "debugpy" +version = "1.8.8" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, + {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"}, + {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"}, + {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"}, + {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"}, + {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"}, + {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"}, + {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"}, + {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"}, + {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"}, + {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"}, + {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"}, + {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"}, + {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"}, + {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"}, + {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"}, + {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"}, + {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"}, + {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"}, + {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"}, + {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"}, + {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"}, + {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"}, + {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"}, + {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"}, + {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "dill" +version = "0.3.9" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "executing" +version = "2.1.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +files = [ + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +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.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +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)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -50,6 +454,169 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipykernel" +version = "6.29.5" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -74,6 +641,101 @@ 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 = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +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" @@ -85,56 +747,355 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "packaging" -version = "24.0" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {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 = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "6.1.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, +] + +[package.extras] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pylint" +version = "3.3.1" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "pylint-3.3.1-py3-none-any.whl", hash = "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9"}, + {file = "pylint-3.3.1.tar.gz", hash = "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e"}, +] + +[package.dependencies] +astroid = ">=3.3.4,<=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\" and python_version < \"3.12\""}, +] +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pyperclip" +version = "1.9.0" +description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" +optional = false +python-versions = "*" +files = [ + {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, +] + [[package]] name = "pytest" -version = "8.1.1" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -142,54 +1103,586 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pywin32" +version = "308" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, +] + +[[package]] +name = "pyzmq" +version = "26.2.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +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 = "rich" -version = "13.7.1" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [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.9\""} +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruff" +version = "0.5.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tiktoken" +version = "0.8.0" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tiktoken-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e"}, + {file = "tiktoken-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21"}, + {file = "tiktoken-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560"}, + {file = "tiktoken-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2"}, + {file = "tiktoken-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9"}, + {file = "tiktoken-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005"}, + {file = "tiktoken-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1"}, + {file = "tiktoken-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a"}, + {file = "tiktoken-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d"}, + {file = "tiktoken-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47"}, + {file = "tiktoken-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419"}, + {file = "tiktoken-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99"}, + {file = "tiktoken-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:881839cfeae051b3628d9823b2e56b5cc93a9e2efb435f4cf15f17dc45f21586"}, + {file = "tiktoken-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe9399bdc3f29d428f16a2f86c3c8ec20be3eac5f53693ce4980371c3245729b"}, + {file = "tiktoken-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a58deb7075d5b69237a3ff4bb51a726670419db6ea62bdcd8bd80c78497d7ab"}, + {file = "tiktoken-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2908c0d043a7d03ebd80347266b0e58440bdef5564f84f4d29fb235b5df3b04"}, + {file = "tiktoken-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:294440d21a2a51e12d4238e68a5972095534fe9878be57d905c476017bff99fc"}, + {file = "tiktoken-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:d8f3192733ac4d77977432947d563d7e1b310b96497acd3c196c9bddb36ed9db"}, + {file = "tiktoken-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:02be1666096aff7da6cbd7cdaa8e7917bfed3467cd64b38b1f112e96d3b06a24"}, + {file = "tiktoken-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94ff53c5c74b535b2cbf431d907fc13c678bbd009ee633a2aca269a04389f9a"}, + {file = "tiktoken-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b231f5e8982c245ee3065cd84a4712d64692348bc609d84467c57b4b72dcbc5"}, + {file = "tiktoken-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4177faa809bd55f699e88c96d9bb4635d22e3f59d635ba6fd9ffedf7150b9953"}, + {file = "tiktoken-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5376b6f8dc4753cd81ead935c5f518fa0fbe7e133d9e25f648d8c4dabdd4bad7"}, + {file = "tiktoken-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:18228d624807d66c87acd8f25fc135665617cab220671eb65b50f5d70fa51f69"}, + {file = "tiktoken-0.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17807445f0cf1f25771c9d86496bd8b5c376f7419912519699f3cc4dc5c12e"}, + {file = "tiktoken-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:886f80bd339578bbdba6ed6d0567a0d5c6cfe198d9e587ba6c447654c65b8edc"}, + {file = "tiktoken-0.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6adc8323016d7758d6de7313527f755b0fc6c72985b7d9291be5d96d73ecd1e1"}, + {file = "tiktoken-0.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b591fb2b30d6a72121a80be24ec7a0e9eb51c5500ddc7e4c2496516dd5e3816b"}, + {file = "tiktoken-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:845287b9798e476b4d762c3ebda5102be87ca26e5d2c9854002825d60cdb815d"}, + {file = "tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02"}, + {file = "tiktoken-0.8.0.tar.gz", hash = "sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tomli" -version = "2.0.1" +version = "2.1.0" description = "A lil' TOML parser" optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +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 = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + +[[package]] +name = "tqdm" +version = "4.67.0" +description = "Fast, Extensible Progress Meter" +optional = false python-versions = ">=3.7" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, + {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, ] +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "vulture" +version = "2.13" +description = "Find dead code" +optional = false +python-versions = ">=3.8" +files = [ + {file = "vulture-2.13-py2.py3-none-any.whl", hash = "sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44"}, + {file = "vulture-2.13.tar.gz", hash = "sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +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)"] +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", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.0" -python-versions = ">=3.8, <4.0" -content-hash = "0eef66a33ba0eac9ffd9360e21ed652e0a32a41cbcfb0a8eb107432d7283cd81" +python-versions = "^3.9,<4.0" +content-hash = "d0ae7928e67439eca430100b3dc961f8fe6909406c057bd0e7d31c8ed7b31664" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..efa46ec --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 860297e..6237c80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,58 @@ [tool.poetry] -name = "code2prompt" -version = "0.2.0" -description = "" +name = "patched-code2prompt" +version = "0.9.0.dev3" +description = "A tool to convert code snippets into AI prompts for documentation or explanation purposes." authors = ["Raphael MANSUY "] +license = "MIT" readme = "README.md" +homepage = "https://github.com/raphaelmansuy/code2prompt" +repository = "https://github.com/raphaelmansuy/code2prompt" +keywords = ["ai", "prompt", "code", "documentation","llm"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +include = [ + "code2prompt/templates/**/*", +] +packages = [ + { include = "code2prompt", from = "." } +] [tool.poetry.dependencies] -python = ">=3.8, <4.0" -rich = "^13.7.1" -click = "^8.1.7" +python = "^3.9,<4.0" +rich = "^13.7.1" # For rich text and beautiful formatting +click = "^8.1.7" # For creating beautiful command line interfaces +jinja2 = "^3.1.4" # For template rendering +tiktoken = "^0.8.0" # For tokenization tasks +pyperclip = "^1.9.0" # For clipboard operations +colorama = "^0.4.6" # For colored terminal text output +tqdm = "^4.66.4" +tabulate = "^0.9.0" +pydantic = "^2.8.2" +prompt-toolkit = "^3.0.43" +colorlog = "^6.8.2" [tool.poetry.scripts] -code2prompt = "code2prompt.main:create_markdown_file" +code2prompt = "code2prompt.main:cli" [tool.poetry.group.dev.dependencies] pytest = "^8.1.1" +ipykernel = "^6.29.4" +vulture = "^2.11" +pylint = "^3.2.6" +ruff = "^0.5.5" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.setuptools] +package-data = {"code2prompt" = ["templates/**/*","data/**/*"]} + diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d28e492 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,77 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py38" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/script/detect_dead_code.sh b/script/detect_dead_code.sh new file mode 100755 index 0000000..498ef89 --- /dev/null +++ b/script/detect_dead_code.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +poetry run vulture ../code2prompt \ No newline at end of file diff --git a/tests/normalize_whitespace_1.py b/tests/normalize_whitespace_1.py new file mode 100644 index 0000000..56c9c13 --- /dev/null +++ b/tests/normalize_whitespace_1.py @@ -0,0 +1,6 @@ +import re + + +def normalize_whitespace(text): + """ Normalize the whitespace in a string.""" + return re.sub(r'\s+', ' ', text.strip()) \ No newline at end of file diff --git a/tests/temp_dir.py b/tests/temp_dir.py new file mode 100644 index 0000000..4714056 --- /dev/null +++ b/tests/temp_dir.py @@ -0,0 +1,14 @@ +import pytest + + +import os +from pathlib import Path + + +@pytest.fixture +def temp_dir(tmp_path): + """Fixture to provide a temporary directory for testing.""" + original_cwd = Path.cwd() + os.chdir(tmp_path) + yield tmp_path + os.chdir(original_cwd) \ No newline at end of file diff --git a/tests/test_add_line_numbers.py b/tests/test_add_line_numbers.py new file mode 100644 index 0000000..4493a02 --- /dev/null +++ b/tests/test_add_line_numbers.py @@ -0,0 +1,22 @@ +def test_add_line_numbers(): + # Sample content to test + content = """First line +Second line +Third line""" + + # Expected output with line numbers added + expected_output = """1: First line +2: Second line +3: Third line""" + + # Function to add line numbers + def add_line_numbers(content): + lines = content.split('\n') + numbered_lines = [f"{i + 1}: {line}" for i, line in enumerate(lines)] + return '\n'.join(numbered_lines) + + # Actual output from the function + actual_output = add_line_numbers(content) + + # Assert that the actual output matches the expected output + assert actual_output == expected_output, "Line numbers were not added correctly." \ No newline at end of file diff --git a/tests/test_analyze.py b/tests/test_analyze.py new file mode 100644 index 0000000..1adac23 --- /dev/null +++ b/tests/test_analyze.py @@ -0,0 +1,92 @@ +import pytest +from click.testing import CliRunner +from code2prompt.main import create_markdown_file +from code2prompt.utils.analyzer import analyze_codebase, format_flat_output, get_extension_list +from pathlib import Path +import tempfile + +@pytest.fixture +def temp_codebase(): + with tempfile.TemporaryDirectory() as tmpdirname: + # Create a temporary codebase structure + Path(tmpdirname, "file1.py").touch() + Path(tmpdirname, "file2.js").touch() + Path(tmpdirname, "subfolder").mkdir() + Path(tmpdirname, "subfolder", "file3.py").touch() + Path(tmpdirname, "subfolder", "file4.css").touch() + yield tmpdirname + +def test_analyze_codebase(temp_codebase): + extension_counts, extension_dirs = analyze_codebase(temp_codebase) + assert extension_counts == {".py": 2, ".js": 1, ".css": 1} + assert len(extension_dirs) == 3 + assert len(extension_dirs[".py"]) == 2 # .py files in root and subfolder + assert len(extension_dirs[".js"]) == 1 + assert len(extension_dirs[".css"]) == 1 + +def test_format_flat_output(): + extension_counts = {".py": 2, ".js": 1, ".css": 1} + output = format_flat_output(extension_counts) + assert ".py: 2 files" in output + assert ".js: 1 file" in output + assert ".css: 1 file" in output + +#def test_format_tree_output(temp_codebase): +# _, extension_dirs = analyze_codebase(temp_codebase) +# output = format_tree_output(extension_dirs) +# assert "└── .py" in output +# assert "└── .js" in output +# assert "└── .css" in output +# assert "subfolder" in output + +def test_get_extension_list(): + extension_counts = {".py": 2, ".js": 1, ".css": 1} + extension_list = get_extension_list(extension_counts) + assert extension_list == ".css,.js,.py" + +def test_analyze_command_flat(temp_codebase): + runner = CliRunner() + result = runner.invoke(create_markdown_file, ['--analyze', '-p', temp_codebase]) + assert result.exit_code == 0 + assert ".py: 2 files" in result.output + assert ".js: 1 file" in result.output + assert ".css: 1 file" in result.output + assert "Comma-separated list of extensions:" in result.output + assert ".css,.js,.py" in result.output + +#def test_analyze_command_tree(temp_codebase): +# runner = CliRunner() +# result = runner.invoke(create_markdown_file, ['--analyze', '-p', temp_codebase, '--format', 'tree']) +# assert result.exit_code == 0 +# assert "└── .py" in result.output +# assert "└── .js" in result.output +# assert "└── .css" in result.output +# assert "subfolder" in result.output +# assert "Comma-separated list of extensions:" in result.output +# assert ".css,.js,.py" in result.output + +#def test_analyze_command_multiple_paths(temp_codebase): +# runner = CliRunner() +# with tempfile.TemporaryDirectory() as second_codebase: +# Path(second_codebase, "file5.java").touch() +# result = runner.invoke(create_markdown_file, ['--analyze', '-p', temp_codebase, '-p', second_codebase]) +# assert result.exit_code == 0 +# assert ".py: 2 files" in result.output +# assert ".js: 1 file" in result.output +# assert ".css: 1 file" in result.output +# assert ".java: 1 file" in result.output +# assert "Comma-separated list of extensions:" in result.output +# assert ".css,.java,.js,.py" in result.output + +def test_analyze_command_empty_directory(): + runner = CliRunner() + with tempfile.TemporaryDirectory() as empty_dir: + result = runner.invoke(create_markdown_file, ['--analyze', '-p', empty_dir]) + assert result.exit_code == 0 + assert "No files found" in result.output or result.output.strip() == "" + +def test_analyze_command_nonexistent_directory(): + runner = CliRunner() + result = runner.invoke(create_markdown_file, ['--analyze', '-p', '/nonexistent/directory']) + assert result.exit_code != 0 + assert "Error" in result.output or "does not exist" in result.output \ No newline at end of file diff --git a/tests/test_code2prompt.py b/tests/test_code2prompt.py index 1665995..8f11268 100644 --- a/tests/test_code2prompt.py +++ b/tests/test_code2prompt.py @@ -1,106 +1,5 @@ -import os -import tempfile -from pathlib import Path -from click.testing import CliRunner -from code2prompt.main import create_markdown_file, parse_gitignore, is_ignored, is_filtered, is_binary -def test_parse_gitignore(): - with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file: - temp_file.write("*.pyc\n# Comment\n/dist/\n") - temp_file.flush() - gitignore_path = Path(temp_file.name) - - patterns = parse_gitignore(gitignore_path) - assert patterns == {"*.pyc", "/dist/"} - - os.unlink(temp_file.name) - -def test_is_ignored(): - gitignore_patterns = ["*.pyc", "/dist/"] - base_path = Path("/project") - - assert is_ignored(Path("/project/file.pyc"), gitignore_patterns, base_path) - assert is_ignored(Path("/project/dist/file.txt"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) - -def test_git_directory_is_ignored(): - gitignore_patterns = ["*.pyc", "/dist/", ".git/"] - base_path = Path("/project") - - assert is_ignored(Path("/project/.git/config"), gitignore_patterns, base_path) - assert is_ignored(Path("/project/.git/HEAD"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) - -def test_venv_directory_is_ignored(): - gitignore_patterns = ["*.pyc", "/dist/", ".git/", ".venv/"] - base_path = Path("/project") - - assert is_ignored(Path("/project/.venv/bin/python"), gitignore_patterns, base_path) - assert is_ignored(Path("/project/.venv/lib/site-packages"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) - -def test_directory_is_ignored_no_backslash(): - gitignore_patterns = ["*.pyc", "/dist", ".git", ".venv"] - base_path = Path("/project") - - assert is_ignored(Path("/project/.venv/bin/python"), gitignore_patterns, base_path) - assert is_ignored(Path("/project/.venv/lib/site-packages"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) - - -def test_relative_path_is_ignored(): - gitignore_patterns = ["*.pyc", "/dist/", ".git/", ".venv/", "relative_dir/"] - base_path = Path("/project") - - assert is_ignored(Path("/project/relative_dir/file.txt"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/other_dir/file.txt"), gitignore_patterns, base_path) - -def test_nested_path_is_ignored(): - gitignore_patterns = ["*.pyc", "/dist/", ".git/", ".venv/", "relative_dir/", "nested_dir/*/*"] - base_path = Path("/project") - - assert is_ignored(Path("/project/nested_dir/sub_dir/file.txt"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/nested_dir/file.txt"), gitignore_patterns, base_path) - assert not is_ignored(Path("/project/other_dir/file.txt"), gitignore_patterns, base_path) - -def test_is_filtered(): - assert is_filtered(Path("file.py"), "*.py") - assert not is_filtered(Path("file.txt"), "*.py") - - -def test_is_binary(): - with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', delete=False) as temp_file: - temp_file.write("Text content") - temp_file.flush() - assert not is_binary(Path(temp_file.name)) - os.unlink(temp_file.name) - - with tempfile.NamedTemporaryFile(mode='wb', delete=False) as temp_file: - temp_file.write(b"\x00\x01\x02") - temp_file.flush() - assert is_binary(Path(temp_file.name)) - os.unlink(temp_file.name) - - - - -def test_create_markdown_file(): - runner = CliRunner() - - with tempfile.TemporaryDirectory() as temp_dir: - temp_dir_path = Path(temp_dir) - (temp_dir_path / "file1.py").write_text("print('Hello')") - (temp_dir_path / "file2.txt").write_text("Text content") - (temp_dir_path / ".gitignore").write_text("*.txt") - - result = runner.invoke(create_markdown_file, ['-p', temp_dir]) - assert result.exit_code == 0 - assert "file1.py" in result.output - assert "file2.txt" not in result.output - - output_file = temp_dir_path / "output.md" - result = runner.invoke(create_markdown_file, ['-p', temp_dir, '-o', str(output_file)]) - assert result.exit_code == 0 - assert output_file.exists() - assert "file1.py" in output_file.read_text() - assert "file2.txt" not in output_file.read_text() \ No newline at end of file + + + + diff --git a/tests/test_comment_stripper.py b/tests/test_comment_stripper.py deleted file mode 100644 index ffc1e3e..0000000 --- a/tests/test_comment_stripper.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Tests for the comment_stripper module.""" -import re -from textwrap import dedent -from code2prompt.comment_stripper import ( - strip_c_style_comments, - strip_python_style_comments, - strip_shell_style_comments, - strip_html_style_comments, - strip_matlab_style_comments, - strip_sql_style_comments, - strip_r_style_comments, -) - - -def normalize_whitespace(text): - """ Normalize the whitespace in a string.""" - return re.sub(r'\s+', ' ', text.strip()) - - -def test_strip_c_style_comments(): - """Test the strip_c_style_comments function.""" - code = """ - int main() { - // Single-line comment - /* Multi-line - comment */ - printf("Hello, World!"); // Inline comment - } - """ - expected = """ - int main() { - printf("Hello, World!"); - } - """ - assert normalize_whitespace(strip_c_style_comments(code)) == normalize_whitespace(dedent(expected)) - - -def test_strip_python_style_comments(): - """Test the strip_python_style_comments function.""" - code = """ - def main(): - # Single-line comment - ''' - Multi-line - comment - ''' - print("Hello, World!") # Inline comment - """ - expected = """ - def main(): - print("Hello, World!") - """ - assert normalize_whitespace(strip_python_style_comments(code)) == normalize_whitespace(dedent(expected)) - - -def test_strip_shell_style_comments(): - """Test the strip_shell_style_comments function.""" - code = """ - #!/bin/bash - # Single-line comment - : ' - Multi-line - comment - ' - echo "Hello, World!" # Inline comment - """ - expected = """ - #!/bin/bash - echo "Hello, World!" - """ - assert normalize_whitespace(strip_shell_style_comments(code)) == normalize_whitespace(dedent(expected)) - - -def test_strip_html_style_comments(): - """Test the strip_html_style_comments function.""" - code = """ - - - - - Hello, World! - - """ - expected = """ - - - Hello, World! - - """ - assert normalize_whitespace(strip_html_style_comments(code)) == normalize_whitespace(dedent(expected)) - - -def test_strip_matlab_style_comments(): - """Test the strip_matlab_style_comments function.""" - code = """ - % Single-line comment - function y = foo(x) - % Multi-line - % comment - y = x + 1; % Inline comment - end - """ - expected = """ - function y = foo(x) - y = x + 1; - end - """ - assert normalize_whitespace(strip_matlab_style_comments(code)) == normalize_whitespace(dedent(expected)) - - -def test_strip_sql_style_comments(): - """Test the strip_sql_style_comments function.""" - code = """ - SELECT * - FROM table - -- Single-line comment - /* Multi-line - comment */ - WHERE condition; -- Inline comment - """ - expected = """ - SELECT * - FROM table - WHERE condition; - """ - assert normalize_whitespace(strip_sql_style_comments(code)) == normalize_whitespace(dedent(expected)) - - -def test_strip_r_style_comments(): - """Test the strip_r_style_comments function.""" - code = """ - # Single-line comment - foo <- function(x) { - # Multi-line - # comment - return(x + 1) # Inline comment - } - """ - expected = """ - foo <- function(x) { - return(x + 1) - } - """ - assert normalize_whitespace(strip_r_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_create_markdown_file.py b/tests/test_create_markdown_file.py new file mode 100644 index 0000000..acb3b44 --- /dev/null +++ b/tests/test_create_markdown_file.py @@ -0,0 +1,30 @@ +from code2prompt.main import create_markdown_file + + +from click.testing import CliRunner + + +import tempfile +from pathlib import Path + + +def test_create_markdown_file(): + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + (temp_dir_path / "file1.py").write_text("print('Hello')") + (temp_dir_path / "file2.txt").write_text("Text content") + (temp_dir_path / ".gitignore").write_text("*.txt") + + result = runner.invoke(create_markdown_file, ['-p', temp_dir]) + assert result.exit_code == 0 + assert "file1.py" in result.output + assert "file2.txt" not in result.output + + output_file = temp_dir_path / "output.md" + result = runner.invoke(create_markdown_file, ['-p', temp_dir, '-o', str(output_file)]) + assert result.exit_code == 0 + assert output_file.exists() + assert "file1.py" in output_file.read_text() + assert "file2.txt" not in output_file.read_text() \ No newline at end of file diff --git a/tests/test_create_markdown_with_exclude.py b/tests/test_create_markdown_with_exclude.py new file mode 100644 index 0000000..6fb66a5 --- /dev/null +++ b/tests/test_create_markdown_with_exclude.py @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/test_create_markdown_with_exclude_1.py b/tests/test_create_markdown_with_exclude_1.py new file mode 100644 index 0000000..6aba2ba --- /dev/null +++ b/tests/test_create_markdown_with_exclude_1.py @@ -0,0 +1,30 @@ +from code2prompt.main import create_markdown_file + + +from click.testing import CliRunner + + +import tempfile +from pathlib import Path + + +def test_create_markdown_with_exclude(): + runner = CliRunner() + with tempfile.TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + (temp_dir_path / "file1.py").write_text("print('Hello')") + (temp_dir_path / "file2.py").write_text("print('World')") + (temp_dir_path / "file3.txt").write_text("Text content") + (temp_dir_path / "ignore_me.py").write_text("# This should be ignored") + + exclude_option = "**/ignore_me.py" + output_file = temp_dir_path / "output_with_exclude.md" + result = runner.invoke(create_markdown_file, ['-p', temp_dir, '-o', str(output_file), '-e', exclude_option]) + + assert result.exit_code == 0 + assert output_file.exists() + output_content = output_file.read_text() + assert "file1.py" in output_content + assert "file2.py" in output_content + assert "file3.txt" in output_content # Assuming we want to include non-Python files by default + assert "ignore_me.py" not in output_content # Ensuring excluded file is not in the output \ No newline at end of file diff --git a/tests/test_create_markdown_with_filter.py b/tests/test_create_markdown_with_filter.py new file mode 100644 index 0000000..b12e98a --- /dev/null +++ b/tests/test_create_markdown_with_filter.py @@ -0,0 +1,26 @@ +from code2prompt.main import create_markdown_file_command + +from click.testing import CliRunner + +import tempfile +from pathlib import Path + +def test_create_markdown_with_filter(): + runner = CliRunner() + with tempfile.TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + (temp_dir_path / "file1.py").write_text("print('Hello')") + (temp_dir_path / "file2.py").write_text("print('World')") + (temp_dir_path / "file3.txt").write_text("Text content") + (temp_dir_path / ".gitignore").write_text("*.txt") + + filter_option = "*.py" + output_file = temp_dir_path / "output_with_filter.md" + result = runner.invoke(create_markdown_file_command, ['-p', temp_dir, '-o', str(output_file), '-f', filter_option]) + + assert result.exit_code == 0 + assert output_file.exists() + output_content = output_file.read_text() + assert "file1.py" in output_content + assert "file2.py" in output_content + assert "file3.txt" not in output_content # Ensuring .txt files are filtered out \ No newline at end of file diff --git a/tests/test_create_template_directory.py b/tests/test_create_template_directory.py new file mode 100644 index 0000000..d1d8611 --- /dev/null +++ b/tests/test_create_template_directory.py @@ -0,0 +1,109 @@ +import pytest +from pathlib import Path +import tempfile +from unittest.mock import patch +from code2prompt.utils.create_template_directory import create_templates_directory + +@pytest.fixture +def temp_dir(): + with tempfile.TemporaryDirectory() as tmpdirname: + yield Path(tmpdirname) + +@pytest.fixture +def mock_package_templates(temp_dir): + package_templates = temp_dir / "package_templates" + package_templates.mkdir() + (package_templates / "template1.j2").write_text("Template 1 content") + (package_templates / "template2.j2").write_text("Template 2 content") + return package_templates + +def test_create_new_templates(temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + create_templates_directory(mock_package_templates, dest_dir) + assert (dest_dir / "template1.j2").exists() + assert (dest_dir / "template2.j2").exists() + assert (dest_dir / "template1.j2").read_text() == "Template 1 content" + assert (dest_dir / "template2.j2").read_text() == "Template 2 content" + +def test_update_existing_templates(temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + dest_dir.mkdir() + (dest_dir / "template1.j2").write_text("Old content") + create_templates_directory(mock_package_templates, dest_dir, force=True) + assert (dest_dir / "template1.j2").read_text() == "Template 1 content" + assert (dest_dir / "template2.j2").read_text() == "Template 2 content" + +def test_permission_error(temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + dest_dir.mkdir(mode=0o555) # Read-only directory + with pytest.raises(PermissionError): + create_templates_directory(mock_package_templates, dest_dir) + +def test_non_existent_source(temp_dir): + with pytest.raises(FileNotFoundError): + create_templates_directory(temp_dir / "non_existent", temp_dir / "dest") + +@patch('builtins.input', return_value='y') +def test_user_confirmation(mock_input, temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + dest_dir.mkdir() + (dest_dir / "template1.j2").write_text("Old content") + create_templates_directory(mock_package_templates, dest_dir) + mock_input.assert_called_once() + assert (dest_dir / "template1.j2").read_text() == "Template 1 content" + +@patch('builtins.input', return_value='n') +def test_user_rejection(mock_input, temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + dest_dir.mkdir() + (dest_dir / "template1.j2").write_text("Old content") + create_templates_directory(mock_package_templates, dest_dir) + mock_input.assert_called_once() + assert (dest_dir / "template1.j2").read_text() == "Old content" + +@patch('code2prompt.utils.create_template_directory.logger') +def test_user_feedback(mock_logger, temp_dir, mock_package_templates): + create_templates_directory(mock_package_templates, temp_dir / "dest") + assert mock_logger.info.call_count >= 2 # At least two info logs + +def test_dry_run(temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + create_templates_directory(mock_package_templates, dest_dir, dry_run=True) + assert not dest_dir.exists() + +def test_force_overwrite(temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + dest_dir.mkdir() + (dest_dir / "template1.j2").write_text("Old content") + create_templates_directory(mock_package_templates, dest_dir, force=True) + assert (dest_dir / "template1.j2").read_text() == "Template 1 content" + +def test_skip_existing(temp_dir, mock_package_templates): + dest_dir = temp_dir / "dest" + dest_dir.mkdir() + (dest_dir / "template1.j2").write_text("Old content") + create_templates_directory(mock_package_templates, dest_dir, skip_existing=True) + assert (dest_dir / "template1.j2").read_text() == "Old content" + assert (dest_dir / "template2.j2").read_text() == "Template 2 content" + +def test_large_number_of_templates(temp_dir): + package_templates = temp_dir / "package_templates" + package_templates.mkdir() + for i in range(100): + (package_templates / f"template{i}.j2").write_text(f"Template {i} content") + create_templates_directory(package_templates, temp_dir / "dest") + assert len(list((temp_dir / "dest").glob("*.j2"))) == 100 + + +def test_special_characters(temp_dir, mock_package_templates): + (mock_package_templates / "special!@#$%^&*.j2").write_text("Special content") + create_templates_directory(mock_package_templates, temp_dir / "dest") + assert (temp_dir / "dest" / "special!@#$%^&*.j2").exists() + assert (temp_dir / "dest" / "special!@#$%^&*.j2").read_text() == "Special content" + +@patch('shutil.disk_usage') +def test_insufficient_disk_space(mock_disk_usage, temp_dir, mock_package_templates): + mock_disk_usage.return_value = (100, 50, 10) # total, used, free + with pytest.raises(IOError, match="Insufficient disk space"): + create_templates_directory(mock_package_templates, temp_dir / "dest") + diff --git a/tests/test_directory_is_ignored_no_backslash.py b/tests/test_directory_is_ignored_no_backslash.py new file mode 100644 index 0000000..81faaf4 --- /dev/null +++ b/tests/test_directory_is_ignored_no_backslash.py @@ -0,0 +1,13 @@ +from code2prompt.utils.is_ignored import is_ignored + + +from pathlib import Path + + +def test_directory_is_ignored_no_backslash(): + gitignore_patterns = ["*.pyc", "/dist", ".git", ".venv"] + base_path = Path("/project") + + assert is_ignored(Path("/project/.venv/bin/python"), gitignore_patterns, base_path) + assert is_ignored(Path("/project/.venv/lib/site-packages"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) \ No newline at end of file diff --git a/tests/test_generate_markdown_content.py b/tests/test_generate_markdown_content.py new file mode 100644 index 0000000..a95616a --- /dev/null +++ b/tests/test_generate_markdown_content.py @@ -0,0 +1,68 @@ +from code2prompt.utils.generate_markdown_content import generate_markdown_content + +def test_generate_markdown_content(): + # Define sample files data + files_data = [ + { + 'path': 'file1.py', + 'extension': 'py', + 'language': 'python', + 'size': 100, + 'created': '2022-01-01', + 'modified': '2022-01-02', + 'content': 'print("Hello, World!")', + }, + { + 'path': 'file2.txt', + 'extension': 'txt', + 'language': 'unknown', + 'size': 50, + 'created': '2022-01-03', + 'modified': '2022-01-04', + 'content': 'Sample text content', + }, + ] + + # Test with no_codeblock=False + expected_output = ( + "# Table of Contents\n" + "- file1.py\n" + "- file2.txt\n\n" + "## File: file1.py\n\n" + "- Extension: py\n" + "- Language: python\n" + "- Size: 100 bytes\n" + "- Created: 2022-01-01\n" + "- Modified: 2022-01-02\n\n" + "### Code\n\n```python\nprint(\"Hello, World!\")\n```\n\n" + "## File: file2.txt\n\n" + "- Extension: txt\n" + "- Language: unknown\n" + "- Size: 50 bytes\n" + "- Created: 2022-01-03\n" + "- Modified: 2022-01-04\n\n" + "### Code\n\n```unknown\nSample text content\n```\n\n" + ) + assert generate_markdown_content(files_data, no_codeblock=False) == expected_output + + # Test with no_codeblock=True + expected_output_no_codeblock = ( + "# Table of Contents\n" + "- file1.py\n" + "- file2.txt\n\n" + "## File: file1.py\n\n" + "- Extension: py\n" + "- Language: python\n" + "- Size: 100 bytes\n" + "- Created: 2022-01-01\n" + "- Modified: 2022-01-02\n\n" + "### Code\n\nprint(\"Hello, World!\")\n\n" + "## File: file2.txt\n\n" + "- Extension: txt\n" + "- Language: unknown\n" + "- Size: 50 bytes\n" + "- Created: 2022-01-03\n" + "- Modified: 2022-01-04\n\n" + "### Code\n\nSample text content\n\n" + ) + assert generate_markdown_content(files_data, no_codeblock=True) == expected_output_no_codeblock \ No newline at end of file diff --git a/tests/test_git_directory_is_ignored.py b/tests/test_git_directory_is_ignored.py new file mode 100644 index 0000000..616c1cd --- /dev/null +++ b/tests/test_git_directory_is_ignored.py @@ -0,0 +1,13 @@ +from code2prompt.utils.is_ignored import is_ignored + + +from pathlib import Path + + +def test_git_directory_is_ignored(): + gitignore_patterns = ["*.pyc", "/dist/", ".git/"] + base_path = Path("/project") + + assert is_ignored(Path("/project/.git/config"), gitignore_patterns, base_path) + assert is_ignored(Path("/project/.git/HEAD"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) \ No newline at end of file diff --git a/tests/test_include_loader.py b/tests/test_include_loader.py new file mode 100644 index 0000000..fcb38d4 --- /dev/null +++ b/tests/test_include_loader.py @@ -0,0 +1,109 @@ +import pytest +from jinja2 import Environment, TemplateNotFound +from code2prompt.utils.include_loader import IncludeLoader + +@pytest.fixture +def temp_dir(tmp_path): + """Create a temporary directory with some template files.""" + main = tmp_path / "main.j2" + main.write_text("Main: {% include 'sub.j2' %}") + + sub = tmp_path / "sub.j2" + sub.write_text("Sub: {{ variable }}") + + nested1 = tmp_path / "nested1.j2" + nested1.write_text("Nested1: {% include 'nested2.j2' %}") + + nested2 = tmp_path / "nested2.j2" + nested2.write_text("Nested2: {{ deep_variable }}") + + circular1 = tmp_path / "circular1.j2" + circular1.write_text("Circular1: {% include 'circular2.j2' %}") + + circular2 = tmp_path / "circular2.j2" + circular2.write_text("Circular2: {% include 'circular1.j2' %}") + + return tmp_path + +def test_simple_include(temp_dir): + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + template = env.get_template("main.j2") + result = template.render(variable="test") + assert result == "Main: Sub: test" + +def test_nested_include(temp_dir): + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + template = env.get_template("nested1.j2") + result = template.render(deep_variable="deep test") + assert result == "Nested1: Nested2: deep test" + +#def test_circular_include(temp_dir): +# loader = IncludeLoader(str(temp_dir)) +# env = Environment(loader=loader) +# template = env.get_template("circular1.j2") +# with pytest.raises(CircularIncludeError): +# template.render() + +def test_missing_template(temp_dir): + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + with pytest.raises(TemplateNotFound): + env.get_template("non_existent.j2") + +def test_include_stack_reset(temp_dir): + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + template = env.get_template("main.j2") + template.render(variable="test") + assert not hasattr(loader.include_stack, 'stack') or not loader.include_stack.stack + +def test_multiple_includes(temp_dir): + multi = temp_dir / "multi.j2" + multi.write_text("Multi: {% include 'main.j2' %} and {% include 'nested1.j2' %}") + + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + template = env.get_template("multi.j2") + result = template.render(variable="test1", deep_variable="test2") + assert result == "Multi: Main: Sub: test1 and Nested1: Nested2: test2" + +#def test_recursive_include(temp_dir): +# recursive = temp_dir / "recursive.j2" +# recursive.write_text("{% if depth > 0 %}Depth {{ depth }}: {% include 'recursive.j2' %}{% else %}End{% endif %}") +# +# loader = IncludeLoader(str(temp_dir)) +# env = Environment(loader=loader) +# template = env.get_template("recursive.j2") +# result = template.render(depth=3) +# assert result == "Depth 3: Depth 2: Depth 1: End" + +def test_include_with_different_encoding(temp_dir): + utf16_file = temp_dir / "utf16.j2" + utf16_file.write_text("UTF-16: {{ variable }}", encoding='utf-16') + + loader = IncludeLoader(str(temp_dir), encoding='utf-16') + env = Environment(loader=loader) + template = env.get_template("utf16.j2") + result = template.render(variable="test") + assert result == "UTF-16: test" + +def test_list_templates(temp_dir): + loader = IncludeLoader(str(temp_dir)) + templates = loader.list_templates() + assert templates == [] + +def test_get_source_not_found(temp_dir): + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + with pytest.raises(TemplateNotFound): + loader.get_source(env, "non_existent.j2") + +def test_get_source_success(temp_dir): + loader = IncludeLoader(str(temp_dir)) + env = Environment(loader=loader) + source, path, uptodate = loader.get_source(env, "main.j2") + assert source == "Main: {% include 'sub.j2' %}" + assert path == str(temp_dir / "main.j2") + assert uptodate() is True \ No newline at end of file diff --git a/tests/test_is_binary.py b/tests/test_is_binary.py new file mode 100644 index 0000000..591d3d0 --- /dev/null +++ b/tests/test_is_binary.py @@ -0,0 +1,20 @@ +from code2prompt.utils.is_binary import is_binary + + +import os +import tempfile +from pathlib import Path + + +def test_is_binary(): + with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', delete=False) as temp_file: + temp_file.write("Text content") + temp_file.flush() + assert not is_binary(Path(temp_file.name)) + os.unlink(temp_file.name) + + with tempfile.NamedTemporaryFile(mode='wb', delete=False) as temp_file: + temp_file.write(b"\x00\x01\x02") + temp_file.flush() + assert is_binary(Path(temp_file.name)) + os.unlink(temp_file.name) \ No newline at end of file diff --git a/tests/test_is_filtered.py b/tests/test_is_filtered.py new file mode 100644 index 0000000..6272b74 --- /dev/null +++ b/tests/test_is_filtered.py @@ -0,0 +1,81 @@ +import pytest +from pathlib import Path +from code2prompt.utils.is_filtered import is_filtered + +# Removed incorrect import + + +@pytest.mark.parametrize( + "file_path, include_pattern, exclude_pattern, case_sensitive, expected", + [ + (Path("file.txt"), "", "", False, True), + (Path("file.py"), "*.py", "", False, True), + (Path("file.txt"), "*.py", "", False, False), + (Path("file.py"), "", "*.py", False, False), + (Path("file.txt"), "", "*.py", False, True), + (Path("file.py"), "*.py,*.txt", "test_*.py", False, True), + (Path("test_file.py"), "*.py,*.txt", "test_*.py", False, False), + (Path("File.PY"), "*.py", "", True, False), + (Path("File.PY"), "*.py", "", False, True), + # (Path("test/file.py"), "**/test/*.py", "", False, True), + (Path("src/file.py"), "**/test/*.py", "", False, False), + (Path("file.txt"), "*.py,*.js,*.txt", "", False, True), + (Path("file.md"), "*.py,*.js,*.txt", "", False, False), + (Path("test_file.py"), "*.py", "test_*.py", False, False), + (Path(".hidden_file"), "*", "", False, True), + (Path("file_without_extension"), "", "*.*", False, True), + (Path("deeply/nested/directory/file.txt"), "**/*.txt", "", False, True), + (Path("file.txt.bak"), "", "*.bak", False, False), + ( + Path("file.py"), + "syntax_map:*.py", + "", + False, + True, + ), # New test case for syntax map + ( + Path("file.txt"), + "syntax_map:*.py", + "", + False, + False, + ), # New test case for syntax map + ], +) +def test_is_filtered_with_directories(): + assert is_filtered( + Path("src/test"), "**/test", "", False + ) # Removed comparison to True + assert not is_filtered(Path("src/prod"), "**/test", "", False) # Updated to use not + + +def test_is_filtered_empty_patterns(): + assert is_filtered(Path("any_file.txt")) # Removed comparison to True + + +def test_is_filtered_case_sensitivity(): + assert not is_filtered(Path("File.TXT"), "*.txt", "", True) # Updated comparison + assert is_filtered(Path("File.TXT"), "*.txt", "", False) # Unchanged + + +def test_is_filtered_exclude_precedence(): + assert not is_filtered(Path("important_test.py"), "*.py", "*test*", False) + + +# Define test cases +test_cases = [ + (Path(".gitignore"), "", "**/.gitignore", False), # Should be excluded + (Path(".codetopromptrc"), "", "**/.codetopromptrc", False), # Should be excluded + (Path("README.md"), "", "", True), # Should be included + (Path("notes.txt"), "", "**/*.txt", False), # Should be excluded + (Path("file.py"), "*.py", "", True), # Should be included +] + +# Run tests +for file_path, include, exclude, expected in test_cases: + result = is_filtered(file_path, include, exclude) + assert ( + result == expected + ), f"Test failed for {file_path}: expected {expected}, got {result}" + +print("All tests passed!") diff --git a/tests/test_is_ignored.py b/tests/test_is_ignored.py new file mode 100644 index 0000000..62fac9f --- /dev/null +++ b/tests/test_is_ignored.py @@ -0,0 +1,13 @@ +from code2prompt.utils.is_ignored import is_ignored + + +from pathlib import Path + + +def test_is_ignored(): + gitignore_patterns = ["*.pyc", "/dist/"] + base_path = Path("/project") + + assert is_ignored(Path("/project/file.pyc"), gitignore_patterns, base_path) + assert is_ignored(Path("/project/dist/file.txt"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) \ No newline at end of file diff --git a/tests/test_language_inference.py b/tests/test_language_inference.py index 452f686..5d676a4 100644 --- a/tests/test_language_inference.py +++ b/tests/test_language_inference.py @@ -1,27 +1,29 @@ -from code2prompt.language_inference import infer_language +import pytest +from code2prompt.utils.language_inference import infer_language def test_infer_language(): """ Test the infer_language function.""" - assert infer_language("main.c") == "c" - assert infer_language("main.cpp") == "cpp" - assert infer_language("Main.java") == "java" - assert infer_language("script.js") == "javascript" - assert infer_language("Program.cs") == "csharp" - assert infer_language("index.php") == "php" - assert infer_language("main.go") == "go" - assert infer_language("lib.rs") == "rust" - assert infer_language("app.kt") == "kotlin" - assert infer_language("main.swift") == "swift" - assert infer_language("Main.scala") == "scala" - assert infer_language("main.dart") == "dart" - assert infer_language("script.py") == "python" - assert infer_language("script.rb") == "ruby" - assert infer_language("script.pl") == "perl" - assert infer_language("script.sh") == "bash" - assert infer_language("script.ps1") == "powershell" - assert infer_language("index.html") == "html" - assert infer_language("data.xml") == "xml" - assert infer_language("query.sql") == "sql" - assert infer_language("script.m") == "matlab" - assert infer_language("script.r") == "r" - assert infer_language("file.txt") == "unknown" + syntax_map = {} # Define the syntax map as needed + assert infer_language("main.c", syntax_map) == "c" # Added syntax_map argument + assert infer_language("main.cpp", syntax_map) == "cpp" + assert infer_language("Main.java", syntax_map) == "java" + assert infer_language("script.js", syntax_map) == "javascript" + assert infer_language("Program.cs", syntax_map) == "csharp" + assert infer_language("index.php", syntax_map) == "php" + assert infer_language("main.go", syntax_map) == "go" + assert infer_language("lib.rs", syntax_map) == "rust" + assert infer_language("app.kt", syntax_map) == "kotlin" + assert infer_language("main.swift", syntax_map) == "swift" + assert infer_language("Main.scala", syntax_map) == "scala" + assert infer_language("main.dart", syntax_map) == "dart" + assert infer_language("script.py", syntax_map) == "python" + assert infer_language("script.rb", syntax_map) == "ruby" + assert infer_language("script.pl", syntax_map) == "perl" + assert infer_language("script.sh", syntax_map) == "bash" + assert infer_language("script.ps1", syntax_map) == "powershell" + assert infer_language("index.html", syntax_map) == "html" + assert infer_language("data.xml", syntax_map) == "xml" + assert infer_language("query.sql", syntax_map) == "sql" + assert infer_language("script.m", syntax_map) == "matlab" + assert infer_language("script.r", syntax_map) == "r" + assert infer_language("file.txt", syntax_map) == "plaintext" diff --git a/tests/test_nested_path_is_ignored.py b/tests/test_nested_path_is_ignored.py new file mode 100644 index 0000000..6fd82b8 --- /dev/null +++ b/tests/test_nested_path_is_ignored.py @@ -0,0 +1,13 @@ +from code2prompt.utils.is_ignored import is_ignored + + +from pathlib import Path + + +def test_nested_path_is_ignored(): + gitignore_patterns = ["*.pyc", "/dist/", ".git/", ".venv/", "relative_dir/", "nested_dir/*/*"] + base_path = Path("/project") + + assert is_ignored(Path("/project/nested_dir/sub_dir/file.txt"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/nested_dir/file.txt"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/other_dir/file.txt"), gitignore_patterns, base_path) \ No newline at end of file diff --git a/tests/test_parse_gitignore.py b/tests/test_parse_gitignore.py new file mode 100644 index 0000000..36e2be0 --- /dev/null +++ b/tests/test_parse_gitignore.py @@ -0,0 +1,18 @@ +from code2prompt.utils.parse_gitignore import parse_gitignore + + +import os +import tempfile +from pathlib import Path + + +def test_parse_gitignore(): + with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file: + temp_file.write("*.pyc\n# Comment\n/dist/\n") + temp_file.flush() + gitignore_path = Path(temp_file.name) + + patterns = parse_gitignore(gitignore_path) + assert patterns == {"*.pyc", "/dist/"} + + os.unlink(temp_file.name) \ No newline at end of file diff --git a/tests/test_price.py b/tests/test_price.py new file mode 100644 index 0000000..cb26b96 --- /dev/null +++ b/tests/test_price.py @@ -0,0 +1,102 @@ +import pytest +from code2prompt.utils.price_calculator import TokenPrices, PriceModel, Provider, calculate_prices, PriceResult + +@pytest.fixture +def sample_token_prices(): + return TokenPrices( + providers=[ + Provider( + name="OpenAI", + models=[ + PriceModel(name="GPT-3", price=0.02), + PriceModel(name="GPT-4", input_price=0.03, output_price=0.06), + ] + ), + Provider( + name="Anthropic", + models=[ + PriceModel(name="Claude", input_price=0.01, output_price=0.03), + ] + ) + ] + ) + +def test_calculate_prices_all_providers_and_models(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500) + assert len(results) == 3 + assert {r.provider_name for r in results} == {"OpenAI", "Anthropic"} + assert {r.model_name for r in results} == {"GPT-3", "GPT-4", "Claude"} + +def test_calculate_prices_specific_provider(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, provider="OpenAI") + assert len(results) == 2 + assert all(r.provider_name == "OpenAI" for r in results) + +def test_calculate_prices_specific_model(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, model="GPT-4") + assert len(results) == 1 + assert results[0].model_name == "GPT-4" + +def test_calculate_prices_non_existent_provider(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, provider="NonExistent") + assert len(results) == 0 + +def test_calculate_prices_non_existent_model(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, model="NonExistent") + assert len(results) == 0 + +def test_calculate_prices_single_price_model(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, model="GPT-3") + assert len(results) == 1 + result = results[0] + assert result.price_input == 0.02 + assert result.price_output == 0.02 + assert result.total_price == pytest.approx(0.03) # (1000 + 500) * 0.02 / 1000 + +def test_calculate_prices_separate_input_output_prices(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, model="GPT-4") + assert len(results) == 1 + result = results[0] + assert result.price_input == 0.03 + assert result.price_output == 0.06 + assert result.total_price == pytest.approx(0.06) # (1000 * 0.03 + 500 * 0.06) / 1000 + +def test_calculate_prices_zero_tokens(sample_token_prices): + results = calculate_prices(sample_token_prices, 0, 0) + assert len(results) == 3 + assert all(r.total_price == 0 for r in results) + + + +def test_calculate_prices_result_structure(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500, model="GPT-4") + assert len(results) == 1 + result = results[0] + assert isinstance(result, PriceResult) + assert hasattr(result, 'provider_name') + assert hasattr(result, 'model_name') + assert hasattr(result, 'price_input') + assert hasattr(result, 'price_output') + assert hasattr(result, 'total_tokens') + assert hasattr(result, 'total_price') + +def test_calculate_prices_total_tokens(sample_token_prices): + results = calculate_prices(sample_token_prices, 1000, 500) + assert all(r.total_tokens == 1500 for r in results) + +@pytest.mark.parametrize("input_tokens,output_tokens,expected_total", [ + (1000, 500, 1500), + (0, 1000, 1000), + (1000, 0, 1000), + (0, 0, 0), +]) +def test_calculate_prices_various_token_combinations(sample_token_prices, input_tokens, output_tokens, expected_total): + results = calculate_prices(sample_token_prices, input_tokens, output_tokens) + assert all(r.total_tokens == expected_total for r in results) + + + +def test_calculate_prices_empty_token_prices(): + empty_token_prices = TokenPrices(providers=[]) + results = calculate_prices(empty_token_prices, 1000, 500) + assert len(results) == 0 diff --git a/tests/test_relative_path_is_ignored.py b/tests/test_relative_path_is_ignored.py new file mode 100644 index 0000000..2c99aef --- /dev/null +++ b/tests/test_relative_path_is_ignored.py @@ -0,0 +1,12 @@ +from code2prompt.utils.is_ignored import is_ignored + + +from pathlib import Path + + +def test_relative_path_is_ignored(): + gitignore_patterns = ["*.pyc", "/dist/", ".git/", ".venv/", "relative_dir/"] + base_path = Path("/project") + + assert is_ignored(Path("/project/relative_dir/file.txt"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/other_dir/file.txt"), gitignore_patterns, base_path) \ No newline at end of file diff --git a/tests/test_strip_c_style_comments.py b/tests/test_strip_c_style_comments.py new file mode 100644 index 0000000..417168f --- /dev/null +++ b/tests/test_strip_c_style_comments.py @@ -0,0 +1,23 @@ +from code2prompt.comment_stripper import strip_c_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_c_style_comments(): + """Test the strip_c_style_comments function.""" + code = """ + int main() { + // Single-line comment + /* Multi-line + comment */ + printf("Hello, World!"); // Inline comment + } + """ + expected = """ + int main() { + printf("Hello, World!"); + } + """ + assert normalize_whitespace(strip_c_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_strip_html_style_comments.py b/tests/test_strip_html_style_comments.py new file mode 100644 index 0000000..98369bd --- /dev/null +++ b/tests/test_strip_html_style_comments.py @@ -0,0 +1,27 @@ +from code2prompt.comment_stripper import strip_html_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_html_style_comments(): + """Test the strip_html_style_comments function.""" + code = """ + + + + + Hello, World! + + """ + expected = """ + + + Hello, World! + + """ + assert normalize_whitespace(strip_html_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_strip_matlab_style_comments.py b/tests/test_strip_matlab_style_comments.py new file mode 100644 index 0000000..d84f8a0 --- /dev/null +++ b/tests/test_strip_matlab_style_comments.py @@ -0,0 +1,23 @@ +from code2prompt.comment_stripper import strip_matlab_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_matlab_style_comments(): + """Test the strip_matlab_style_comments function.""" + code = """ + % Single-line comment + function y = foo(x) + % Multi-line + % comment + y = x + 1; % Inline comment + end + """ + expected = """ + function y = foo(x) + y = x + 1; + end + """ + assert normalize_whitespace(strip_matlab_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_strip_python_style_comments.py b/tests/test_strip_python_style_comments.py new file mode 100644 index 0000000..ab1e81a --- /dev/null +++ b/tests/test_strip_python_style_comments.py @@ -0,0 +1,23 @@ +from code2prompt.comment_stripper import strip_python_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_python_style_comments(): + """Test the strip_python_style_comments function.""" + code = """ + def main(): + # Single-line comment + ''' + Multi-line + comment + ''' + print("Hello, World!") # Inline comment + """ + expected = """ + def main(): + print("Hello, World!") + """ + assert normalize_whitespace(strip_python_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_strip_r_style_comments.py b/tests/test_strip_r_style_comments.py new file mode 100644 index 0000000..567a93c --- /dev/null +++ b/tests/test_strip_r_style_comments.py @@ -0,0 +1,23 @@ +from code2prompt.comment_stripper import strip_r_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_r_style_comments(): + """Test the strip_r_style_comments function.""" + code = """ + # Single-line comment + foo <- function(x) { + # Multi-line + # comment + return(x + 1) # Inline comment + } + """ + expected = """ + foo <- function(x) { + return(x + 1) + } + """ + assert normalize_whitespace(strip_r_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_strip_shell_style_comments.py b/tests/test_strip_shell_style_comments.py new file mode 100644 index 0000000..552c35a --- /dev/null +++ b/tests/test_strip_shell_style_comments.py @@ -0,0 +1,23 @@ +from code2prompt.comment_stripper import strip_shell_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_shell_style_comments(): + """Test the strip_shell_style_comments function.""" + code = """ + #!/bin/bash + # Single-line comment + : ' + Multi-line + comment + ' + echo "Hello, World!" # Inline comment + """ + expected = """ + #!/bin/bash + echo "Hello, World!" + """ + assert normalize_whitespace(strip_shell_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_strip_sql_style_comments.py b/tests/test_strip_sql_style_comments.py new file mode 100644 index 0000000..1494c88 --- /dev/null +++ b/tests/test_strip_sql_style_comments.py @@ -0,0 +1,23 @@ +from code2prompt.comment_stripper import strip_sql_style_comments +from tests.normalize_whitespace_1 import normalize_whitespace + + +from textwrap import dedent + + +def test_strip_sql_style_comments(): + """Test the strip_sql_style_comments function.""" + code = """ + SELECT * + FROM table + -- Single-line comment + /* Multi-line + comment */ + WHERE condition; -- Inline comment + """ + expected = """ + SELECT * + FROM table + WHERE condition; + """ + assert normalize_whitespace(strip_sql_style_comments(code)) == normalize_whitespace(dedent(expected)) \ No newline at end of file diff --git a/tests/test_template_include.py b/tests/test_template_include.py new file mode 100644 index 0000000..b74aec0 --- /dev/null +++ b/tests/test_template_include.py @@ -0,0 +1,104 @@ +from code2prompt.core.template_processor import process_template + +def test_include_feature(tmp_path): + # Create a main template + main_template = tmp_path / "main.j2" + main_template.write_text("Main: {% include 'sub.j2' %}") + + # Create a sub-template + sub_template = tmp_path / "sub.j2" + sub_template.write_text("Sub: {{ variable }}") + + template_content = main_template.read_text() + files_data = [] + user_inputs = {"variable": "test"} + + result = process_template(template_content, files_data, user_inputs, str(main_template)) + assert result == "Main: Sub: test" + +def test_nested_include(tmp_path): + # Create a main template + main_template = tmp_path / "main.j2" + main_template.write_text("Main: {% include 'sub1.j2' %}") + + # Create sub-templates + sub1_template = tmp_path / "sub1.j2" + sub1_template.write_text("Sub1: {% include 'sub2.j2' %}") + + sub2_template = tmp_path / "sub2.j2" + sub2_template.write_text("Sub2: {{ variable }}") + + template_content = main_template.read_text() + files_data = [] + user_inputs = {"variable": "nested"} + + result = process_template(template_content, files_data, user_inputs, str(main_template)) + assert result == "Main: Sub1: Sub2: nested" + +def test_multiple_includes(tmp_path): + # Create a main template + main_template = tmp_path / "main.j2" + main_template.write_text("Main: {% include 'sub1.j2' %} and {% include 'sub2.j2' %}") + + # Create sub-templates + sub1_template = tmp_path / "sub1.j2" + sub1_template.write_text("Sub1: {{ var1 }}") + + sub2_template = tmp_path / "sub2.j2" + sub2_template.write_text("Sub2: {{ var2 }}") + + template_content = main_template.read_text() + files_data = [] + user_inputs = {"var1": "first", "var2": "second"} + + result = process_template(template_content, files_data, user_inputs, str(main_template)) + assert result == "Main: Sub1: first and Sub2: second" + +def test_include_with_context(tmp_path): + # Create a main template + main_template = tmp_path / "main.j2" + main_template.write_text("Main: {% include 'sub.j2' %}") + + # Create a sub-template + sub_template = tmp_path / "sub.j2" + sub_template.write_text("Sub: {{ main_var }} and {{ sub_var }}") + + template_content = main_template.read_text() + files_data = [] + user_inputs = {"main_var": "from main", "sub_var": "from sub"} + + result = process_template(template_content, files_data, user_inputs, str(main_template)) + assert result == "Main: Sub: from main and from sub" + + + +def test_include_with_files_data(tmp_path): + # Create a main template + main_template = tmp_path / "main.j2" + main_template.write_text("Main: {% include 'sub.j2' %}") + + # Create a sub-template + sub_template = tmp_path / "sub.j2" + sub_template.write_text("Sub: {{ files[0].name }}") + + template_content = main_template.read_text() + files_data = [{"name": "test_file.py", "content": "print('Hello')"}] + user_inputs = {} + + result = process_template(template_content, files_data, user_inputs, str(main_template)) + assert result == "Main: Sub: test_file.py" + +#def test_circular_include(tmp_path): + # Create templates with circular inclusion +# template1 = tmp_path / "template1.j2" +# template1.write_text("T1: {% include 'template2.j2' %}") + +# template2 = tmp_path / "template2.j2" +# template2.write_text("T2: {% include 'template1.j2' %}") + +# template_content = template1.read_text() +# files_data = [] +# user_inputs = {} + +# with pytest.raises(ValueError, match="Circular include detected"): +# process_template(template_content, files_data, user_inputs, str(template1)) \ No newline at end of file diff --git a/tests/test_template_processor.py b/tests/test_template_processor.py new file mode 100644 index 0000000..85d787f --- /dev/null +++ b/tests/test_template_processor.py @@ -0,0 +1,110 @@ +import pytest +from unittest.mock import patch +from code2prompt.core.template_processor import get_user_inputs + +@pytest.fixture +def mock_prompt(): + with patch('code2prompt.core.template_processor.prompt') as mock: + yield mock + +def test_get_user_inputs_single_variable(mock_prompt): + mock_prompt.return_value = "test_value" + template_content = "This is a {{input:variable}} test." + result = get_user_inputs(template_content) + assert result == {"variable": "test_value"} + mock_prompt.assert_called_once_with("Enter value for variable: ") + +def test_get_user_inputs_multiple_variables(mock_prompt): + mock_prompt.side_effect = ["value1", "value2"] + template_content = "{{input:var1}} and {{input:var2}} are two variables." + result = get_user_inputs(template_content) + assert result == {"var1": "value1", "var2": "value2"} + assert mock_prompt.call_count == 2 + +def test_get_user_inputs_duplicate_variables(mock_prompt): + mock_prompt.return_value = "repeated_value" + template_content = "{{input:var}} appears twice: {{input:var}}" + result = get_user_inputs(template_content) + assert result == {"var": "repeated_value"} + mock_prompt.assert_called_once_with("Enter value for var: ") + +def test_get_user_inputs_no_variables(mock_prompt): + template_content = "This template has no input variables." + result = get_user_inputs(template_content) + assert result == {} + mock_prompt.assert_not_called() + +def test_get_user_inputs_whitespace_in_variable_names(mock_prompt): + mock_prompt.side_effect = ["value1", "value2"] + template_content = "{{ input:var1 }} and {{ input:var2 }} have whitespace." + result = get_user_inputs(template_content) + assert result == {"var1": "value1", "var2": "value2"} + assert mock_prompt.call_count == 2 + +def test_get_user_inputs_case_sensitivity(mock_prompt): + mock_prompt.side_effect = ["value1", "value2"] + template_content = "{{input:VAR}} and {{input:var}} are different." + result = get_user_inputs(template_content) + assert result == {"VAR": "value1", "var": "value2"} + assert mock_prompt.call_count == 2 + +def test_get_user_inputs_special_characters(mock_prompt): + mock_prompt.return_value = "special_value" + template_content = "This is a {{input:special!@#$%^&*()_+}} variable." + result = get_user_inputs(template_content) + assert result == {"special!@#$%^&*()_+": "special_value"} + mock_prompt.assert_called_once_with("Enter value for special!@#$%^&*()_+: ") + +def test_get_user_inputs_empty_variable_name(mock_prompt): + template_content = "This has an {{input:}} empty variable name." + result = get_user_inputs(template_content) + assert result == {} + mock_prompt.assert_not_called() + +def test_get_user_inputs_malformed_variables(mock_prompt): + template_content = "Malformed {{input:var} and {input:var}} variables." + result = get_user_inputs(template_content) + assert result == {} + mock_prompt.assert_not_called() + +def test_get_user_inputs_ignore_jinja_execute_blocks(mock_prompt): + template_content = """ + {% if condition %} + {{var}} + {% endif %} + {{input:user_var}} + {% for item in items %} + {{item}} + {% endfor %} + """ + mock_prompt.return_value = "user_value" + result = get_user_inputs(template_content) + assert result == {"user_var": "user_value"} + mock_prompt.assert_called_once_with("Enter value for user_var: ") + +def test_get_user_inputs_mixed_variables(mock_prompt): + template_content = """ + Regular variable: {{var}} + Input variable: {{input:user_var}} + {% if condition %} + Jinja block variable: {{block_var}} + {% endif %} + Another input: {{input:another_var}} + """ + mock_prompt.side_effect = ["user_value", "another_value"] + result = get_user_inputs(template_content) + assert result == {"user_var": "user_value", "another_var": "another_value"} + assert mock_prompt.call_count == 2 + +def test_get_user_inputs_nested_jinja_blocks(mock_prompt): + template_content = """ + {% if outer_condition %} + {% for item in items %} + {{input:user_var}} + {% endfor %} + {% endif %} + """ + mock_prompt.return_value = "user_value" + result = get_user_inputs(template_content) + assert result == {"user_var": "user_value"} + mock_prompt.assert_called_once_with("Enter value for user_var: ") \ No newline at end of file diff --git a/tests/test_venv_directory_is_ignored.py b/tests/test_venv_directory_is_ignored.py new file mode 100644 index 0000000..2779861 --- /dev/null +++ b/tests/test_venv_directory_is_ignored.py @@ -0,0 +1,13 @@ +from code2prompt.utils.is_ignored import is_ignored + + +from pathlib import Path + + +def test_venv_directory_is_ignored(): + gitignore_patterns = ["*.pyc", "/dist/", ".git/", ".venv/"] + base_path = Path("/project") + + assert is_ignored(Path("/project/.venv/bin/python"), gitignore_patterns, base_path) + assert is_ignored(Path("/project/.venv/lib/site-packages"), gitignore_patterns, base_path) + assert not is_ignored(Path("/project/file.txt"), gitignore_patterns, base_path) \ No newline at end of file diff --git a/todo/improvements.md b/todo/improvements.md new file mode 100644 index 0000000..94cba9f --- /dev/null +++ b/todo/improvements.md @@ -0,0 +1,19 @@ +# Suggested Improvements for Code2Prompt + +## 1. Documentation +- Ensure all functions and classes have comprehensive docstrings that explain their purpose, parameters, and return values. + +## 2. Type Annotations +- Add type annotations to all function parameters and return types for better readability and static type checking. + +## 3. Logging +- Implement logging in critical areas of the code to help with debugging and monitoring. + +## 4. Error Handling +- Improve error handling to provide more informative messages and handle exceptions gracefully. + +## 5. Code Structure +- Consider breaking down larger functions into smaller, more manageable ones to improve readability and maintainability. + +## 6. Testing +- Ensure that there are adequate unit tests covering all functionalities, especially for edge cases.