From 1814fdf2986444a5b06ff4a71586dea0b94d78a9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 4 Jun 2025 09:39:28 +0545 Subject: [PATCH] initial --- .gitignore | 48 +++++++ README.md | 241 +++++++++++++++++++++++++++++++ __init__.py | 5 + devtools/__init__.py | 5 + devtools/changelog/__init__.py | 9 ++ devtools/changelog/cli.py | 111 ++++++++++++++ devtools/changelog/generator.py | 139 ++++++++++++++++++ devtools/cli.py | 77 ++++++++++ devtools/commitgen/__init__.py | 15 ++ devtools/commitgen/cli.py | 247 ++++++++++++++++++++++++++++++++ devtools/commitgen/generator.py | 79 ++++++++++ devtools/commitgen/git.py | 41 ++++++ devtools/gitignore/__init__.py | 13 ++ devtools/gitignore/cli.py | 84 +++++++++++ devtools/gitignore/detector.py | 161 +++++++++++++++++++++ devtools/gitignore/generator.py | 106 ++++++++++++++ devtools/shared/__init__.py | 9 ++ devtools/shared/ai.py | 107 ++++++++++++++ devtools/shared/config.py | 134 +++++++++++++++++ devtools/shared/git.py | 231 +++++++++++++++++++++++++++++ requirements.txt | 4 + setup.py | 40 ++++++ uv.lock | 211 +++++++++++++++++++++++++++ 23 files changed, 2117 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 devtools/__init__.py create mode 100644 devtools/changelog/__init__.py create mode 100644 devtools/changelog/cli.py create mode 100644 devtools/changelog/generator.py create mode 100644 devtools/cli.py create mode 100644 devtools/commitgen/__init__.py create mode 100644 devtools/commitgen/cli.py create mode 100644 devtools/commitgen/generator.py create mode 100644 devtools/commitgen/git.py create mode 100644 devtools/gitignore/__init__.py create mode 100644 devtools/gitignore/cli.py create mode 100644 devtools/gitignore/detector.py create mode 100644 devtools/gitignore/generator.py create mode 100644 devtools/shared/__init__.py create mode 100644 devtools/shared/ai.py create mode 100644 devtools/shared/config.py create mode 100644 devtools/shared/git.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebbf8a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Generic .gitignore +# Generated by devtools gitignore generator + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Build directories +build/ +dist/ +out/ +target/ + +# Dependencies +node_modules/ +venv/ +env/ +__pycache__/ +*.pyc + +# Logs +*.log +logs/ + +# Environment variables +.env +.env.local +.env.*.local + +# Temporary files +*.tmp +*.temp +*.bak + +devtools.egg-info/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c3aaa1 --- /dev/null +++ b/README.md @@ -0,0 +1,241 @@ +# DevTools + +A collection of developer tools to enhance your workflow. This package includes tools for generating commit messages, changelogs, and .gitignore files, all accessible from a single unified CLI: `devtools`. + +## Installation + +```bash +# Clone the repository +$ git clone https://github.com/S4NKALP/DevTools.git +$ cd devtools + +# Install in editable mode +$ pip install -e . +``` + +## Configuration + +### API Keys + +The tools use various APIs that require API keys. You can set these using the config command: + +```bash +# Set OpenRouter API key (for AI features) +devtools config set OPENROUTER_API_KEY "your-openrouter-api-key" + +# Set GitHub API key (optional, for enhanced features) +devtools config set github_api_key "your-github-api-key" +``` + +Or set them in your environment: + +```bash +# OpenRouter API key +export OPENROUTER_API_KEY="your-openrouter-api-key" + +# GitHub API key +export GITHUB_API_KEY="your-github-api-key" +``` + +Or create a `.env` file in your project root: + +```bash +OPENROUTER_API_KEY=your-openrouter-api-key +GITHUB_API_KEY=your-github-api-key +``` + +### Managing Configuration + +```bash +# Show all configuration +devtools config show + +# Show specific value +devtools config show OPENROUTER_API_KEY + +# Delete a value +devtools config delete OPENROUTER_API_KEY + +# Clear all configuration +devtools config clear +``` + +Configuration files are stored in: + +- `~/.devtools/config.yaml` + +Example config.yaml: + +```yaml +# API Keys +OPENROUTER_API_KEY: + +# AI Settings +max_tokens: 1024 +temperature: 0.7 +output_format: text + +# Commit Settings +conventional_commits: true + +# Repository Settings +repositories: [] +``` + +## Usage + +All tools are available under the main `devtools` command: + +``` +devtools [COMMAND] [SUBCOMMAND] [OPTIONS] +``` + +### Commit Message Generator + +Generate and apply commit messages, create changelogs, and manage configuration. + +#### Generate Commit Message + +```bash +devtools commit generate [OPTIONS] +``` + +**Options:** + +- `--files, -f [FILE]...` Specific files to generate commit messages for +- `--repo, -r PATH` Repository path (default: current directory) +- `--commit, -c` Automatically commit changes +- `--push, -p` Push changes after commit +- `--conventional/--no-conventional` Use conventional commit format (default: True) +- `--no-stage` Skip automatic staging of changes +- `--sign` Sign commits with GPG +- `--temperature FLOAT` AI temperature (0.0-1.0) + +Commit messages follow the conventional format with emojis: + +- ✨ feat: New feature +- 🐛 fix: Bug fix +- 📚 docs: Documentation +- 💅 style: Formatting, missing semi colons, etc +- ♻️ refactor: Code refactoring +- ✅ test: Adding tests +- 🔧 chore: Maintenance + +#### Changelog Generation + +```bash +# Generate from git history +devtools commit changelog generate [OPTIONS] + +# Generate interactively +devtools changelog generate [OPTIONS] + +# Generate from a file +devtools changelog generate-from-file [OPTIONS] +``` + +**Options:** + +- `--version, -v VERSION` Version number for the changelog entry (required) +- `--from-tag, -t TAG` Generate changelog from this tag +- `--days, -d N` Generate changelog from the last N days +- `--commits, -n N` Generate changelog from the last N commits +- `--output, -o FILE` Output file path (default: CHANGELOG.md) +- `--temperature FLOAT` AI temperature (0.0-1.0) +- `--input, -i FILE` Input file with changes (for file-based generation) + +The changelog generator supports three modes: + +1. Git history: Automatically generates changelog from commit history +2. Interactive: Enter changes manually with emoji prefixes +3. File-based: Generate from a file containing changes + +Changes are grouped by type with emojis: + +- ✨ Added +- 🔄 Changed +- 🐛 Fixed +- 🚀 Performance +- 📝 Documentation +- 🔧 Maintenance +- 🗑️ Removed +- 🔒 Security + +#### Repository Status + +```bash +devtools commit status +``` + +--- + +### Gitignore Generator + +Generate .gitignore files for your project, either by auto-detecting project types or specifying technologies. + +#### Auto-detect Project Types + +```bash +devtools gitignore auto [OPTIONS] +``` + +**Options:** + +- `--directory, -d DIR` Directory to scan (default: current directory) +- `--output, -o FILE` Output file path (default: .gitignore) + +#### Generate for Specific Technologies + +```bash +devtools gitignore generate python django vscode [OPTIONS] +``` + +**Options:** + +- `--output, -o FILE` Output file path (default: .gitignore) + +The generator uses the gitignore.io API to fetch templates. No API key is required. + +--- + +## Features + +- **Commit Message Generator:** + - AI-powered commit message suggestions using Mixtral 8x7B + - Conventional commit format with emojis + - GPG signing and push support + - Changelog generation from commit history + - Repository status with diff previews +- **Gitignore Generator:** + - Auto-detects project types and generates .gitignore + - Uses gitignore.io API for up-to-date templates + - No API key required + - Falls back to generic template if API is unavailable + +## Requirements + +- Python 3.8+ +- Git +- OpenRouter API key (for AI features) +- GitHub API key (optional) + +## Development + +```bash +# Install dependencies +pip install -r requirements.txt + +# Run tests +pytest + +# Lint +ruff . +``` + +## Contributing + +Contributions are welcome! Please open issues or submit pull requests. + +## License + +This project is licensed under the GNU General Public License v3.0 (GPL-3.0) - see the LICENSE file for details. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..16b3278 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +""" +Commit message generator module +""" + +__version__ = "0.1.0" \ No newline at end of file diff --git a/devtools/__init__.py b/devtools/__init__.py new file mode 100644 index 0000000..0a4e421 --- /dev/null +++ b/devtools/__init__.py @@ -0,0 +1,5 @@ +""" +DevTools - A collection of developer tools. +""" + +__version__ = "0.1.0" \ No newline at end of file diff --git a/devtools/changelog/__init__.py b/devtools/changelog/__init__.py new file mode 100644 index 0000000..f1ee095 --- /dev/null +++ b/devtools/changelog/__init__.py @@ -0,0 +1,9 @@ +""" +Changelog generation using AI. +""" + +from .generator import ChangelogGenerator + +__all__ = [ + "ChangelogGenerator" +] \ No newline at end of file diff --git a/devtools/changelog/cli.py b/devtools/changelog/cli.py new file mode 100644 index 0000000..9f2a0a7 --- /dev/null +++ b/devtools/changelog/cli.py @@ -0,0 +1,111 @@ +""" +Changelog generation CLI. +""" +import click +from rich.console import Console +from rich.prompt import Prompt, Confirm +from rich.progress import Progress, SpinnerColumn, TextColumn +from .generator import ChangelogGenerator +from ..commitgen.config import CommitGenConfig + +console = Console() + +@click.group() +def cli(): + """Generate changelogs.""" + pass + +@cli.command() +@click.option("--version", "-v", required=True, help="Version number") +@click.option("--output", "-o", default="CHANGELOG.md", help="Output file path") +@click.option("--temperature", "-t", type=float, default=0.7, help="AI temperature") +def generate(version: str, output: str, temperature: float): + """Generate a changelog interactively.""" + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + # Initialize services + progress.add_task("Initializing...", total=None) + config = CommitGenConfig() + generator = ChangelogGenerator(config) + + # Collect changes + console.print("\n[bold]Enter your changes (one per line). Start each line with an emoji to indicate the type:[/]") + console.print("✨ Added") + console.print("🔄 Changed") + console.print("🐛 Fixed") + console.print("🚀 Performance") + console.print("📝 Documentation") + console.print("🔧 Maintenance") + console.print("🗑️ Removed") + console.print("🔒 Security") + console.print("\nPress Ctrl+D (Unix) or Ctrl+Z (Windows) when done.\n") + + changes_text = [] + while True: + try: + line = Prompt.ask("") + if line: + changes_text.append(line) + except EOFError: + break + + if not changes_text: + console.print("[yellow]No changes provided. Exiting.") + return + + # Parse changes + progress.add_task("Parsing changes...", total=None) + changes = generator.parse_changes_from_text("\n".join(changes_text)) + + # Generate changelog + progress.add_task("Generating changelog...", total=None) + generator.update_changelog_file(version, changes, output, temperature) + + console.print(f"\n[green]Generated changelog at {output}") + +@cli.command() +@click.option("--version", "-v", required=True, help="Version number") +@click.option("--input", "-i", required=True, help="Input file with changes") +@click.option("--output", "-o", default="CHANGELOG.md", help="Output file path") +@click.option("--temperature", "-t", type=float, default=0.7, help="AI temperature") +def generate_from_file(version: str, input: str, output: str, temperature: float): + """Generate a changelog from a file.""" + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + # Initialize services + progress.add_task("Initializing...", total=None) + config = CommitGenConfig() + generator = ChangelogGenerator(config) + + # Read changes from file + progress.add_task("Reading changes...", total=None) + try: + with open(input, "r") as f: + changes_text = f.read() + except FileNotFoundError: + console.print(f"[red]Input file not found: {input}") + return + + # Parse changes + progress.add_task("Parsing changes...", total=None) + changes = generator.parse_changes_from_text(changes_text) + + if not changes: + console.print("[yellow]No changes found in input file.") + return + + # Generate changelog + progress.add_task("Generating changelog...", total=None) + generator.update_changelog_file(version, changes, output, temperature) + + console.print(f"\n[green]Generated changelog at {output}") + +# Add command aliases +cli.add_command(generate, name="g") +cli.add_command(generate_from_file, name="f") \ No newline at end of file diff --git a/devtools/changelog/generator.py b/devtools/changelog/generator.py new file mode 100644 index 0000000..73ee376 --- /dev/null +++ b/devtools/changelog/generator.py @@ -0,0 +1,139 @@ +""" +Changelog generation using AI. +""" +from typing import List, Dict, Optional +from ..shared.ai import AIService +from ..shared.config import Config +from ..shared.git import GitService + +class ChangelogGenerator(AIService): + """AI-powered changelog generator.""" + + def __init__(self, config: Config): + """Initialize changelog generator.""" + super().__init__(config) + + def generate_changelog(self, version: str, changes: List[Dict[str, str]], temperature: Optional[float] = None) -> str: + """Generate a changelog from a list of changes. + + Args: + version: Version number for the changelog + changes: List of changes, each with 'type' and 'description' + temperature: AI temperature for generation + + Returns: + Generated changelog content + """ + system_prompt = """You are a helpful AI that generates changelogs. + Follow these rules: + 1. Group changes by type (Added, Changed, Fixed, etc.) + 2. Keep descriptions concise but informative + 3. Use past tense + 4. Start each entry with a verb + 5. Use emojis for each type: + - ✨ Added + - 🔄 Changed + - 🐛 Fixed + - 🚀 Performance + - 📝 Documentation + - 🔧 Maintenance + - 🗑️ Removed + - 🔒 Security""" + + # Format changes for the prompt + changes_text = "\n".join( + f"{change['type']}: {change['description']}" + for change in changes + ) + + user_prompt = f"Generate a changelog for version {version} with these changes:\n\n{changes_text}" + + return self.generate_completion( + system_prompt, + user_prompt, + temperature=temperature + ) + + def update_changelog_file(self, version: str, changes: List[Dict[str, str]], output_file: str, temperature: Optional[float] = None) -> None: + """Generate and update a changelog file. + + Args: + version: Version number for the changelog + changes: List of changes, each with 'type' and 'description' + output_file: Path to the changelog file + temperature: AI temperature for generation + """ + changelog_content = self.generate_changelog(version, changes, temperature) + + # Read existing changelog if it exists + try: + with open(output_file, "r") as f: + existing_content = f.read() + except FileNotFoundError: + existing_content = "" + + # Add new changelog entry at the top + new_content = f"## {version}\n\n{changelog_content}\n\n" + if existing_content: + new_content += existing_content + + # Write updated changelog + with open(output_file, "w") as f: + f.write(new_content) + + def parse_changes_from_text(self, text: str) -> List[Dict[str, str]]: + """Parse changes from text input. + + Args: + text: Text containing changes + + Returns: + List of changes with type and description + """ + changes = [] + current_type = None + current_description = [] + + for line in text.splitlines(): + line = line.strip() + if not line: + continue + + # Check if line starts with a type marker + type_markers = { + "✨": "Added", + "🔄": "Changed", + "🐛": "Fixed", + "🚀": "Performance", + "📝": "Documentation", + "🔧": "Maintenance", + "🗑️": "Removed", + "🔒": "Security" + } + + for marker, type_ in type_markers.items(): + if line.startswith(marker): + # Save previous change if exists + if current_type and current_description: + changes.append({ + "type": current_type, + "description": " ".join(current_description) + }) + + # Start new change + current_type = type_ + current_description = [line[len(marker):].strip()] + break + else: + # Continue current change + if current_type: + current_description.append(line) + + # Add last change if exists + if current_type and current_description: + changes.append({ + "type": current_type, + "description": " ".join(current_description) + }) + + return changes \ No newline at end of file diff --git a/devtools/cli.py b/devtools/cli.py new file mode 100644 index 0000000..b53ce0e --- /dev/null +++ b/devtools/cli.py @@ -0,0 +1,77 @@ +""" +Main CLI for devtools. +""" +import click +from rich.console import Console +from rich.table import Table +from .shared.config import Config +from .gitignore.cli import cli as gitignore_cli +from .commitgen.cli import cli as commit_cli + +console = Console() + +@click.group() +def cli(): + """Developer tools CLI.""" + pass + +@cli.group() +def config(): + """Manage configuration.""" + pass + +@config.command() +@click.argument("key") +@click.argument("value") +def set(key: str, value: str): + """Set a configuration value.""" + config = Config() + config.set(key, value) + console.print(f"[green]Set {key} to {value}") + +@config.command() +@click.argument("key", required=False) +def show(key: str = None): + """Show configuration values.""" + config = Config() + if key: + value = config.get(key) + if value is None: + console.print(f"[yellow]No value set for {key}") + else: + console.print(f"{key}: {value}") + else: + table = Table(title="Configuration") + table.add_column("Key", style="cyan") + table.add_column("Value", style="green") + + for k, v in config.get_all().items(): + table.add_row(k, str(v)) + + console.print(table) + +@config.command() +@click.argument("key") +def delete(key: str): + """Delete a configuration value.""" + config = Config() + if key in config.get_all(): + config.delete(key) + console.print(f"[green]Deleted {key}") + else: + console.print(f"[yellow]No value set for {key}") + +@config.command() +def clear(): + """Clear all configuration values.""" + if click.confirm("Are you sure you want to clear all configuration?"): + config = Config() + config.clear() + console.print("[green]Cleared all configuration") + +# Add subcommands +cli.add_command(gitignore_cli, name="gitignore") +cli.add_command(commit_cli, name="commit") + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/devtools/commitgen/__init__.py b/devtools/commitgen/__init__.py new file mode 100644 index 0000000..eca22ca --- /dev/null +++ b/devtools/commitgen/__init__.py @@ -0,0 +1,15 @@ +""" +Commit message generator using AI. +""" + +from ..shared.config import Config +from .generator import CommitGenerator +from .git import CommitGenGitService +from ..changelog import ChangelogGenerator + +__all__ = [ + "Config", + "CommitGenerator", + "CommitGenGitService", + "ChangelogGenerator" +] \ No newline at end of file diff --git a/devtools/commitgen/cli.py b/devtools/commitgen/cli.py new file mode 100644 index 0000000..295075f --- /dev/null +++ b/devtools/commitgen/cli.py @@ -0,0 +1,247 @@ +""" +Command-line interface for commitgen tool. +""" +import click +from rich.console import Console +from rich.prompt import Prompt +from rich.panel import Panel +from rich.table import Table +from rich.syntax import Syntax +from rich.progress import Progress, SpinnerColumn, TextColumn +from ..shared.config import Config +from .git import CommitGenGitService +from .generator import CommitGenerator +from ..changelog import ChangelogGenerator +from ..shared.git import GitService +import os + +console = Console() + +@click.group() +def cli(): + """Generate commit messages and changelogs using AI.""" + pass + +@cli.command() +@click.option('--files', '-f', multiple=True, help='Specific files to generate commit messages for') +@click.option('--repo', '-r', help='Repository path') +@click.option('--commit', '-c', is_flag=True, help='Automatically commit changes') +@click.option('--push', '-p', is_flag=True, help='Push changes after commit') +@click.option('--conventional/--no-conventional', default=True, help='Use conventional commit format') +@click.option('--no-stage', is_flag=True, help='Skip automatic staging of changes') +@click.option('--sign', is_flag=True, help='Sign commits with GPG') +@click.option('--temperature', type=float, help='AI temperature (0.0-1.0)') +def generate(files: tuple, repo: str, commit: bool, push: bool, conventional: bool, no_stage: bool, sign: bool, temperature: float): + """Generate commit messages for staged changes""" + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + # Initialize services + config = Config() + git_service = CommitGenGitService(config) + ai_service = CommitGenerator(config) + + # Stage all changes if not disabled + if not no_stage: + task = progress.add_task("Staging changes...", total=None) + git_service.stage_all_changes() + progress.update(task, completed=True) + + # Get staged changes + task = progress.add_task("Analyzing changes...", total=None) + staged_changes = git_service.get_staged_changes(files) + progress.update(task, completed=True) + + if not staged_changes: + console.print("[yellow]No staged changes found.[/yellow]") + return + + # Generate commit message + task = progress.add_task("Generating commit message...", total=None) + commit_message = ai_service.generate_commit_message(staged_changes, temperature) + progress.update(task, completed=True) + + # Show preview and confirm + console.print("\n[bold]Generated commit message:[/bold]") + console.print(Panel(commit_message, title="Preview", border_style="blue")) + + if commit: + if Prompt.ask("\nApply this commit message?", choices=["y", "n"], default="y") == "y": + task = progress.add_task("Committing changes...", total=None) + git_service.commit_changes(commit_message, sign=sign) + progress.update(task, completed=True) + + if push: + task = progress.add_task("Pushing changes...", total=None) + git_service.push_changes() + progress.update(task, completed=True) + + console.print("[green]Changes committed successfully![/green]") + else: + console.print("[yellow]Commit cancelled.[/yellow]") + else: + console.print("[yellow]Use --commit to apply the commit message.[/yellow]") + + except Exception as e: + console.print(f"[red]Error: {str(e)}[/red]") + raise click.Abort() + +@cli.group() +def changelog(): + """Generate and manage changelogs""" + pass + +@changelog.command(name='generate') +@click.option('--version', '-v', required=True, help='Version number for the changelog entry') +@click.option('--from-tag', '-t', help='Generate changelog from this tag') +@click.option('--days', '-d', type=int, help='Generate changelog from the last N days') +@click.option('--commits', '-n', type=int, help='Generate changelog from the last N commits') +@click.option('--output', '-o', help='Output file path (default: CHANGELOG.md)') +@click.option('--temperature', type=float, help='AI temperature (0.0-1.0)') +def generate_log(version: str, from_tag: str, days: int, commits: int, output: str, temperature: float): + """Generate a changelog from git history""" + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + # Initialize services + config = Config() + git_service = CommitGenGitService(config) + changelog_gen = ChangelogGenerator(config, git_service) + + task = progress.add_task("Analyzing commit history...", total=None) + + # Determine which method to use for getting commits + if from_tag: + entries = changelog_gen.get_commits(from_tag) + elif days: + entries = changelog_gen.get_commits_since_date(days) + elif commits: + entries = changelog_gen.get_last_n_commits(commits) + else: + # Default: get commits from the last 7 days + entries = changelog_gen.get_commits_since_date(7) + + progress.update(task, completed=True) + + if not entries: + console.print("[yellow]No conventional commits found in the specified range.[/yellow]") + return + + task = progress.add_task("Generating changelog...", total=None) + output_path = output if output else "CHANGELOG.md" + changelog_gen.update_changelog_file(version, entries, output_path) + progress.update(task, completed=True) + + console.print(f"[green]Changelog generated successfully at {output_path}![/green]") + + except Exception as e: + console.print(f"[red]Error: {str(e)}[/red]") + raise click.Abort() + +@cli.group() +def config(): + """Manage configuration""" + pass + +@config.command() +@click.option('--key', '-k', required=True, help='Configuration key') +@click.option('--value', '-v', required=True, help='Configuration value') +def set(key: str, value: str): + """Set configuration value""" + try: + config = Config() + config.set(key, value) + console.print(f"[green]Configuration updated: {key}={value}[/green]") + except Exception as e: + console.print(f"[red]Error: {str(e)}[/red]") + raise click.Abort() + +@config.command() +def show(): + """Show current configuration""" + try: + config = Config() + console.print(Panel( + "\n".join(f"{k}: {v}" for k, v in config.get_all().items()), + title="[blue]Current Configuration[/blue]", + border_style="blue" + )) + except Exception as e: + console.print(f"[red]Error: {str(e)}[/red]") + raise click.Abort() + +@cli.command() +def status(): + """Show the current state of the repository.""" + try: + # Initialize services + git = GitService(os.getcwd()) + + # Get repository info + repo_name = git.get_repo_name() + current_branch = git.get_current_branch() + + # Get changes + staged_files = git.get_staged_files() + unstaged_files = git.get_unstaged_files() + + # Create status table + table = Table(title=f"Repository Status: {repo_name} ({current_branch})") + table.add_column("Status", style="cyan") + table.add_column("Files", style="green") + + # Add staged changes + if staged_files: + staged_content = "\n".join(f" • {file}" for file in staged_files) + table.add_row("Staged", staged_content) + else: + table.add_row("Staged", "No staged changes") + + # Add unstaged changes + if unstaged_files: + unstaged_content = "\n".join(f" • {file}" for file in unstaged_files) + table.add_row("Unstaged", unstaged_content) + else: + table.add_row("Unstaged", "No unstaged changes") + + # Show status + console.print("\n[bold]Repository Status:[/bold]") + console.print(table) + + # Show diffs if there are changes + if staged_files or unstaged_files: + console.print("\n[bold]Changes:[/bold]") + + # Show staged diffs + if staged_files: + console.print("\n[cyan]Staged Changes:[/cyan]") + for file in staged_files: + diff = git.get_diff(file, staged=True) + if diff: + console.print(Syntax(diff, "diff", theme="monokai")) + + # Show unstaged diffs + if unstaged_files: + console.print("\n[yellow]Unstaged Changes:[/yellow]") + for file in unstaged_files: + diff = git.get_diff(file, staged=False) + if diff: + console.print(Syntax(diff, "diff", theme="monokai")) + + except Exception as e: + console.print(f"[red]Error: {str(e)}[/red]") + raise click.Abort() + +# Add command aliases +cli.add_command(generate, name='g') +cli.add_command(config, name='c') +cli.add_command(changelog, name='log') + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/devtools/commitgen/generator.py b/devtools/commitgen/generator.py new file mode 100644 index 0000000..b724ac0 --- /dev/null +++ b/devtools/commitgen/generator.py @@ -0,0 +1,79 @@ +""" +Commit message and changelog generation using AI. +""" +from typing import List, Dict, Optional +from ..shared.ai import AIService +from ..shared.config import Config + +class CommitGenerator(AIService): + """AI-powered commit message and changelog generator.""" + + def __init__(self, config: Config): + """Initialize commit generator.""" + super().__init__(config) + + def generate_commit_message(self, diff: str, temperature: Optional[float] = None) -> str: + """Generate a commit message from a diff.""" + system_prompt = """You are a helpful AI that generates git commit messages. Keep responses concise and follow the conventional commit format. + + Format: type(scope): description + Types and their emojis: + - feat: ✨ (new feature) + - fix: 🐛 (bug fix) + - docs: 📚 (documentation) + - style: 💅 (formatting, missing semi colons, etc) + - refactor: ♻️ (refactoring code) + - test: ✅ (adding tests) + - chore: 🔧 (maintenance) + + The message should be a single line and follow the conventional commit format strictly. Include the appropriate emoji at the start of the message.""" + + user_prompt = f"Write a conventional commit message for these changes:\n\n{diff}" + + message = self.generate_completion( + system_prompt, + user_prompt, + temperature=temperature + ) + + # Clean up the response to ensure it's a valid commit message + message = message.split('\n')[0].strip() # Take only the first line + + # Ensure the message starts with an emoji + if not any(emoji in message for emoji in ['✨', '🐛', '📚', '💅', '♻️', '✅', '🔧']): + # Add appropriate emoji based on the commit type + if message.startswith('feat'): + message = '✨ ' + message + elif message.startswith('fix'): + message = '🐛 ' + message + elif message.startswith('docs'): + message = '📚 ' + message + elif message.startswith('style'): + message = '💅 ' + message + elif message.startswith('refactor'): + message = '♻️ ' + message + elif message.startswith('test'): + message = '✅ ' + message + elif message.startswith('chore'): + message = '🔧 ' + message + else: + message = '🔧 ' + message # Default to chore + + return message + + def generate_changelog(self, commits: List[str], version: str, temperature: Optional[float] = None) -> str: + """Generate a changelog from a list of commits.""" + system_prompt = """You are a helpful AI that generates changelogs. + Follow these rules: + 1. Group changes by type (Added, Changed, Fixed, etc.) + 2. Keep descriptions concise but informative + 3. Use past tense + 4. Start each entry with a verb""" + + user_prompt = f"Generate a changelog for version {version} with these commits:\n\n" + "\n".join(commits) + + return self.generate_completion( + system_prompt, + user_prompt, + temperature=temperature + ) \ No newline at end of file diff --git a/devtools/commitgen/git.py b/devtools/commitgen/git.py new file mode 100644 index 0000000..e3d8473 --- /dev/null +++ b/devtools/commitgen/git.py @@ -0,0 +1,41 @@ +""" +Git service for commit_gen tool. +""" +from typing import List, Optional +from ..shared.git import GitService +from ..shared.config import Config +import os + +class CommitGenGitService(GitService): + """Git service for commit_gen tool.""" + + def __init__(self, config: Config): + """Initialize commit_gen git service.""" + super().__init__(os.getcwd()) # Use current directory as repo path + self.config = config + + def get_staged_changes(self, files: Optional[List[str]] = None) -> str: + """Get staged changes as a diff. + + Args: + files: Optional list of files to get changes for + """ + diffs = self.get_all_diffs(staged=True) + if files: + diffs = {file: diff for file, diff in diffs.items() if file in files} + return "\n\n".join(f"Changes in {file}:\n{diff}" for file, diff in diffs.items()) + + def get_unstaged_changes(self) -> str: + """Get unstaged changes as a diff.""" + diffs = self.get_all_diffs(staged=False) + return "\n\n".join(f"Changes in {file}:\n{diff}" for file, diff in diffs.items()) + + def commit_changes(self, message: str, sign: bool = False) -> None: + """Commit changes with the given message.""" + if not self.commit(message, sign=sign): + raise Exception("Failed to commit changes") + + def get_recent_commits(self, limit: int = 10) -> List[str]: + """Get recent commit messages.""" + commits = self.get_commit_history(limit=limit) + return [commit["message"] for commit in commits] \ No newline at end of file diff --git a/devtools/gitignore/__init__.py b/devtools/gitignore/__init__.py new file mode 100644 index 0000000..0f12f6e --- /dev/null +++ b/devtools/gitignore/__init__.py @@ -0,0 +1,13 @@ +""" +Gitignore generator module. +""" + +from .cli import cli +from .generator import GitignoreGenerator +from .detector import ProjectDetector + +__all__ = [ + "cli", + "GitignoreGenerator", + "ProjectDetector" +] \ No newline at end of file diff --git a/devtools/gitignore/cli.py b/devtools/gitignore/cli.py new file mode 100644 index 0000000..ed1af09 --- /dev/null +++ b/devtools/gitignore/cli.py @@ -0,0 +1,84 @@ +""" +Gitignore generation CLI. +""" +import os +import click +from rich.console import Console +from rich.prompt import Confirm +from rich.progress import Progress, SpinnerColumn, TextColumn +from .generator import GitignoreGenerator +from .detector import ProjectDetector + +console = Console() + +@click.group() +def cli(): + """Generate .gitignore files.""" + pass + +@cli.command() +@click.option("--directory", "-d", default=".", help="Directory to scan") +@click.option("--output", "-o", default=".gitignore", help="Output file path") +def auto(directory: str, output: str): + """Automatically detect project types and generate .gitignore.""" + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + # Initialize services + progress.add_task("Initializing...", total=None) + detector = ProjectDetector(directory) + generator = GitignoreGenerator() + + # Detect project types + progress.add_task("Detecting project types...", total=None) + detected_types = detector.detect_project_types() + + if not detected_types: + console.print("[yellow]No project types detected. Using generic .gitignore.") + else: + console.print(f"\n[green]Detected project types:[/] {', '.join(detected_types)}") + + # Check if output file exists + if os.path.exists(output): + if not Confirm.ask(f"\n[yellow]File {output} already exists. Overwrite?"): + return + + # Generate .gitignore + progress.add_task("Generating .gitignore...", total=None) + generator.update_gitignore_file(detected_types, output) + + console.print(f"\n[green]Generated .gitignore at {output}") + +@cli.command() +@click.argument("types", nargs=-1, required=True) +@click.option("--output", "-o", default=".gitignore", help="Output file path") +def generate(types: tuple, output: str): + """Generate .gitignore for specified types.""" + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + # Initialize services + progress.add_task("Initializing...", total=None) + generator = GitignoreGenerator() + + # Check if output file exists + if os.path.exists(output): + if not Confirm.ask(f"\n[yellow]File {output} already exists. Overwrite?"): + return + + # Generate .gitignore + progress.add_task("Generating .gitignore...", total=None) + generator.update_gitignore_file(list(types), output) + + console.print(f"\n[green]Generated .gitignore at {output}") + +# Add command aliases +cli.add_command(auto, name="a") +cli.add_command(generate, name="g") + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/devtools/gitignore/detector.py b/devtools/gitignore/detector.py new file mode 100644 index 0000000..37a0273 --- /dev/null +++ b/devtools/gitignore/detector.py @@ -0,0 +1,161 @@ +""" +Project type detection for gitignore generation. +""" +import os +from typing import List, Dict, Set + +class ProjectDetector: + """Detect project types based on file patterns.""" + + # File patterns for different project types + PROJECT_PATTERNS = { + # Languages + "python": ["*.py", "requirements.txt", "setup.py", "Pipfile", "pyproject.toml"], + "javascript": ["package.json", "*.js", "*.jsx", "*.ts", "*.tsx"], + "typescript": ["tsconfig.json", "*.ts", "*.tsx"], + "java": ["*.java", "pom.xml", "build.gradle", "*.class"], + "kotlin": ["*.kt", "*.kts"], + "go": ["go.mod", "go.sum", "*.go"], + "rust": ["Cargo.toml", "Cargo.lock", "*.rs"], + "ruby": ["Gemfile", "*.rb", "*.gemspec"], + "php": ["composer.json", "*.php"], + "csharp": ["*.cs", "*.csproj", "*.sln"], + "swift": ["*.swift", "*.xcodeproj"], + "objective-c": ["*.m", "*.h", "*.mm"], + + # Frameworks + "django": ["manage.py", "wsgi.py", "asgi.py"], + "flask": ["app.py", "flask_app.py"], + "fastapi": ["main.py", "app.py"], + "react": ["src/App.js", "src/App.tsx"], + "vue": ["vue.config.js", "*.vue"], + "angular": ["angular.json", "tsconfig.json"], + "spring": ["application.properties", "application.yml"], + "laravel": ["artisan", "composer.json"], + "rails": ["config/routes.rb", "Gemfile"], + "express": ["app.js", "server.js"], + "next": ["next.config.js"], + "nuxt": ["nuxt.config.js"], + + # Build Tools + "maven": ["pom.xml"], + "gradle": ["build.gradle", "settings.gradle"], + "npm": ["package.json"], + "yarn": ["yarn.lock"], + "pip": ["requirements.txt"], + "poetry": ["pyproject.toml"], + "cargo": ["Cargo.toml"], + "composer": ["composer.json"], + + # IDEs and Editors + "vscode": [".vscode/settings.json"], + "intellij": [".idea/workspace.xml"], + "eclipse": [".project", ".classpath"], + "vim": [".vimrc", ".vim/"], + "emacs": [".emacs", ".emacs.d/"], + "sublime": ["*.sublime-project", "*.sublime-workspace"], + "atom": [".atom/"], + + # Operating Systems + "windows": ["*.exe", "*.dll", "*.sys"], + "macos": [".DS_Store", "*.app"], + "linux": ["*.so", "*.o"], + + # Testing Frameworks + "jest": ["jest.config.js"], + "pytest": ["pytest.ini", "conftest.py"], + "junit": ["junit.xml"], + "mocha": ["mocha.opts"], + + # Documentation + "sphinx": ["docs/conf.py"], + "mkdocs": ["mkdocs.yml"], + "docusaurus": ["docusaurus.config.js"], + + # Databases + "postgresql": ["*.sql", "*.dump"], + "mysql": ["*.sql", "*.dump"], + "mongodb": ["*.bson"], + "sqlite": ["*.db", "*.sqlite"], + + # Cloud Platforms + "aws": [".aws/", "aws.json"], + "azure": ["azure.yaml", "azure.json"], + "gcp": ["gcloud.json"], + + # Containerization + "docker": ["Dockerfile", "docker-compose.yml"], + "kubernetes": ["k8s/", "*.yaml"], + + # CI/CD + "github-actions": [".github/workflows/"], + "gitlab-ci": [".gitlab-ci.yml"], + "jenkins": ["Jenkinsfile"], + "travis": [".travis.yml"], + "circleci": [".circleci/config.yml"], + + # Static Site Generators + "jekyll": ["_config.yml"], + "hugo": ["config.toml", "config.yaml"], + "gatsby": ["gatsby-config.js"], + + # Game Development + "unity": ["*.unity", "*.unityproj"], + "unreal": ["*.uproject"], + + # Machine Learning + "tensorflow": ["*.pb", "*.h5"], + "pytorch": ["*.pt", "*.pth"], + "scikit-learn": ["*.joblib"] + } + + def __init__(self, directory: str = "."): + """Initialize project detector. + + Args: + directory: Directory to scan + """ + self.directory = directory + + def detect_project_types(self) -> List[str]: + """Detect project types in the directory. + + Returns: + List of detected project types + """ + detected_types = set() + + # Scan directory for files + for root, _, files in os.walk(self.directory): + for file in files: + file_path = os.path.join(root, file) + rel_path = os.path.relpath(file_path, self.directory) + + # Check each project type's patterns + for type_, patterns in self.PROJECT_PATTERNS.items(): + if any(self._matches_pattern(rel_path, pattern) for pattern in patterns): + detected_types.add(type_) + + return sorted(list(detected_types)) + + def _matches_pattern(self, path: str, pattern: str) -> bool: + """Check if a path matches a pattern. + + Args: + path: Path to check + pattern: Pattern to match + + Returns: + True if path matches pattern + """ + # Handle directory patterns + if pattern.endswith("/"): + return path.startswith(pattern[:-1]) + + # Handle wildcard patterns + if "*" in pattern: + import fnmatch + return fnmatch.fnmatch(path, pattern) + + # Handle exact matches + return path == pattern \ No newline at end of file diff --git a/devtools/gitignore/generator.py b/devtools/gitignore/generator.py new file mode 100644 index 0000000..7098951 --- /dev/null +++ b/devtools/gitignore/generator.py @@ -0,0 +1,106 @@ +""" +Gitignore generator using gitignore.io API. +""" +import os +import requests +from typing import List, Dict + +class GitignoreGenerator: + """Gitignore generator using gitignore.io API.""" + + def __init__(self): + """Initialize gitignore generator.""" + self.api_url = "https://www.toptal.com/developers/gitignore/api" + + def generate_gitignore(self, types: List[str]) -> str: + """Generate .gitignore content for specified types. + + Args: + types: List of project types + + Returns: + Generated .gitignore content + """ + if not types: + return self._get_generic_gitignore() + + try: + # Get gitignore content from API + response = requests.get(f"{self.api_url}/{','.join(types)}") + response.raise_for_status() + + # Add header + header = "# Generated by devtools gitignore generator\n" + header += f"# Types: {', '.join(types)}\n\n" + + return header + response.text + + except requests.RequestException: + # Fallback to generic gitignore if API fails + return self._get_generic_gitignore() + + def _get_generic_gitignore(self) -> str: + """Get a generic .gitignore template. + + Returns: + Generic .gitignore content + """ + return """# Generic .gitignore +# Generated by devtools gitignore generator + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Build directories +build/ +dist/ +out/ +target/ + +# Dependencies +node_modules/ +venv/ +env/ +__pycache__/ +*.pyc + +# Logs +*.log +logs/ + +# Environment variables +.env +.env.local +.env.*.local + +# Temporary files +*.tmp +*.temp +*.bak +""" + + def update_gitignore_file(self, types: List[str], output_file: str) -> None: + """Generate and update a .gitignore file. + + Args: + types: List of project types + output_file: Path to the .gitignore file + """ + content = self.generate_gitignore(types) + + # Write to file + with open(output_file, "w") as f: + f.write(content) \ No newline at end of file diff --git a/devtools/shared/__init__.py b/devtools/shared/__init__.py new file mode 100644 index 0000000..48c483c --- /dev/null +++ b/devtools/shared/__init__.py @@ -0,0 +1,9 @@ +""" +Shared components for devtools package. +""" + +from .config import BaseConfig +from .git import GitService +from .ai import AIService + +__all__ = ["BaseConfig", "GitService", "AIService"] \ No newline at end of file diff --git a/devtools/shared/ai.py b/devtools/shared/ai.py new file mode 100644 index 0000000..8ec354c --- /dev/null +++ b/devtools/shared/ai.py @@ -0,0 +1,107 @@ +""" +Shared AI service for devtools. +""" +import requests +import os +from typing import List, Dict, Optional, Any +from .config import BaseConfig + +class AIService: + """Base AI service that can be extended by specific tools.""" + + def __init__(self, config: BaseConfig): + """Initialize AI service with configuration.""" + self.config = config + self._setup_ai() + + def _setup_ai(self) -> None: + """Set up the AI model with configuration.""" + self.api_key = self.config.get_env_or_config("OPENROUTER_API_KEY") + if not self.api_key: + raise ValueError("OpenRouter API key not found. Please set it in config or environment.") + + self.model = self.config.get("model", "mistralai/mixtral-8x7b-instruct") + self.api_url = "https://openrouter.ai/api/v1/chat/completions" + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "HTTP-Referer": "https://github.com/S4NKALP/DevTools", + "X-Title": "DevTools", + "Content-Type": "application/json" + } + + def _create_prompt(self, system_prompt: str, user_prompt: str) -> List[Dict[str, str]]: + """Create a prompt for the AI model.""" + return [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + + def generate_completion(self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95) -> str: + """Generate a completion from the AI model.""" + messages = self._create_prompt(system_prompt, user_prompt) + + try: + # Convert config values to proper types + temp = temperature if temperature is not None else float(self.config.get("temperature", 0.7)) + tokens = max_tokens if max_tokens is not None else int(self.config.get("max_tokens", 150)) + + payload = { + "model": self.model, + "messages": messages, + "temperature": temp, + "max_tokens": tokens, + "top_p": float(top_p), + "stream": False + } + + # Remove None values + payload = {k: v for k, v in payload.items() if v is not None} + + response = requests.post( + self.api_url, + headers=self.headers, + json=payload, + timeout=30 + ) + + if response.status_code != 200: + error_msg = response.text + try: + error_json = response.json() + if "error" in error_json: + error_msg = error_json["error"] + except: + pass + raise Exception(f"OpenRouter API error: {error_msg}") + + result = response.json() + + if "choices" in result and len(result["choices"]) > 0: + return result["choices"][0]["message"]["content"].strip() + + return "" + + except Exception as e: + raise Exception(f"AI generation failed: {str(e)}") + + def generate_batch_completions(self, + system_prompt: str, + prompts: List[str], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None) -> List[str]: + """Generate completions for multiple prompts.""" + completions = [] + for prompt in prompts: + completion = self.generate_completion( + system_prompt, + prompt, + temperature, + max_tokens + ) + completions.append(completion) + return completions \ No newline at end of file diff --git a/devtools/shared/config.py b/devtools/shared/config.py new file mode 100644 index 0000000..6c9a0a4 --- /dev/null +++ b/devtools/shared/config.py @@ -0,0 +1,134 @@ +""" +Shared configuration system. +""" +import os +import yaml +from pathlib import Path +from typing import Any, Dict, Optional + +class BaseConfig: + """Base configuration class.""" + + def __init__(self, default_config: Dict[str, Any] = None): + """Initialize base configuration. + + Args: + default_config: Default configuration values + """ + self._config = default_config or {} + + def get(self, key: str, default: Any = None) -> Any: + """Get a configuration value. + + Args: + key: Configuration key + default: Default value if key not found + + Returns: + Configuration value + """ + return self._config.get(key, default) + + def set(self, key: str, value: Any) -> None: + """Set a configuration value. + + Args: + key: Configuration key + value: Configuration value + """ + self._config[key] = value + + def delete(self, key: str) -> None: + """Delete a configuration value. + + Args: + key: Configuration key + """ + if key in self._config: + del self._config[key] + + def get_all(self) -> Dict[str, Any]: + """Get all configuration values. + + Returns: + Dictionary of all configuration values + """ + return self._config.copy() + + def clear(self) -> None: + """Clear all configuration values.""" + self._config = {} + + def validate(self) -> bool: + """Override in subclasses for validation logic.""" + return True + + def get_env_or_config(self, key: str, env_var: Optional[str] = None) -> Optional[str]: + """Get value from environment variable or config. + + Args: + key: Configuration key + env_var: Environment variable name (defaults to key.upper()) + """ + env_var = env_var or key.upper() + return self._config.get(key) or os.getenv(env_var) + +class Config(BaseConfig): + """Configuration manager for devtools.""" + + def __init__(self): + """Initialize configuration.""" + super().__init__() + self.config_dir = self._get_config_dir() + self.config_file = self.config_dir / "config.yaml" + self._ensure_config_dir() + self._load_config() + + def _get_config_dir(self) -> Path: + """Get the configuration directory. + + Returns: + Path to config directory + """ + return Path.home() / ".devtools" + + def _ensure_config_dir(self) -> None: + """Ensure configuration directory exists.""" + self.config_dir.mkdir(parents=True, exist_ok=True) + + def _load_config(self) -> None: + """Load configuration from file.""" + if self.config_file.exists(): + with open(self.config_file, "r") as f: + self._config = yaml.safe_load(f) or {} + else: + self._config = {} + + def _save_config(self) -> None: + """Save configuration to file.""" + with open(self.config_file, "w") as f: + yaml.dump(self._config, f, default_flow_style=False, sort_keys=False) + + def set(self, key: str, value: Any) -> None: + """Set a configuration value. + + Args: + key: Configuration key + value: Configuration value + """ + super().set(key, value) + self._save_config() + + def delete(self, key: str) -> None: + """Delete a configuration value. + + Args: + key: Configuration key + """ + super().delete(key) + self._save_config() + + def clear(self) -> None: + """Clear all configuration values.""" + super().clear() + self._save_config() \ No newline at end of file diff --git a/devtools/shared/git.py b/devtools/shared/git.py new file mode 100644 index 0000000..6894d76 --- /dev/null +++ b/devtools/shared/git.py @@ -0,0 +1,231 @@ +""" +Shared Git service for devtools. +""" +import subprocess +from pathlib import Path +from typing import List, Dict, Optional, Tuple +import os +from datetime import datetime, timedelta + +class GitService: + """Base Git service that can be extended by specific tools.""" + + def __init__(self, repo_path: str): + """Initialize GitService with repository path.""" + self.repo_path = repo_path + if not os.path.exists(os.path.join(repo_path, '.git')): + raise ValueError(f"Not a git repository: {repo_path}") + + def _is_git_repo(self) -> bool: + """Check if the current directory is a git repository.""" + try: + subprocess.run( + ["git", "rev-parse", "--is-inside-work-tree"], + cwd=self.repo_path, + check=True, + capture_output=True + ) + return True + except subprocess.CalledProcessError: + return False + + def _run_git_command(self, args: List[str]) -> Tuple[str, str]: + """Run a git command and return its output.""" + try: + result = subprocess.run( + ["git"] + args, + cwd=self.repo_path, + check=True, + capture_output=True, + text=True + ) + return result.stdout.strip(), result.stderr.strip() + except subprocess.CalledProcessError as e: + return e.stdout.strip(), e.stderr.strip() + + def get_staged_files(self) -> List[str]: + """Get list of staged files.""" + stdout, _ = self._run_git_command(["diff", "--cached", "--name-only"]) + return stdout.splitlines() if stdout else [] + + def get_unstaged_files(self) -> List[str]: + """Get list of unstaged files.""" + stdout, _ = self._run_git_command(["ls-files", "--modified", "--others", "--exclude-standard"]) + return stdout.splitlines() if stdout else [] + + def get_diff(self, file_path: str, staged: bool = True) -> str: + """Get diff for a specific file.""" + args = ["diff", "--cached" if staged else ""] + if file_path: + args.append(file_path) + stdout, _ = self._run_git_command(args) + return stdout + + def get_all_diffs(self, staged: bool = True) -> Dict[str, str]: + """Get diffs for all changed files.""" + files = self.get_staged_files() if staged else self.get_unstaged_files() + return {file: self.get_diff(file, staged) for file in files} + + def commit(self, message: str, sign: bool = False) -> bool: + """Create a commit with the given message.""" + args = ["commit"] + if sign: + args.append("-S") + args.extend(["-m", message]) + stdout, stderr = self._run_git_command(args) + return not stderr + + def get_repo_name(self) -> str: + """Get the repository name.""" + stdout, _ = self._run_git_command(["rev-parse", "--show-toplevel"]) + return Path(stdout).name + + def get_current_branch(self) -> str: + """Get the current branch name.""" + stdout, _ = self._run_git_command(["rev-parse", "--abbrev-ref", "HEAD"]) + return stdout + + def push(self, branch: Optional[str] = None) -> bool: + """Push changes to remote.""" + if not branch: + branch = self.get_current_branch() + stdout, stderr = self._run_git_command(["push", "origin", branch]) + return not stderr + + def get_staged_changes(self, files: Optional[List[str]] = None) -> Dict[str, str]: + """Get staged changes for specified files or all staged changes.""" + try: + diffs = {} # Initialize diffs at the start + + if files: + for file in files: + result = subprocess.run( + ['git', 'diff', '--staged', '--', file], + cwd=self.repo_path, + capture_output=True, + text=True + ) + if result.stdout: + diffs[file] = result.stdout + else: + result = subprocess.run( + ['git', 'diff', '--staged'], + cwd=self.repo_path, + capture_output=True, + text=True + ) + if result.stdout: + # Parse the diff to get individual file changes + current_file = None + current_diff = [] + + for line in result.stdout.split('\n'): + if line.startswith('diff --git'): + if current_file and current_diff: + diffs[current_file] = '\n'.join(current_diff) + current_file = line.split('b/')[-1].strip() + current_diff = [line] + elif current_file: + current_diff.append(line) + + if current_file and current_diff: + diffs[current_file] = '\n'.join(current_diff) + + return diffs + except subprocess.CalledProcessError as e: + raise Exception(f"Failed to get staged changes: {str(e)}") + + def stage_all_changes(self) -> None: + """Stage all changes in the repository.""" + try: + subprocess.run( + ['git', 'add', '.'], + cwd=self.repo_path, + check=True + ) + except subprocess.CalledProcessError as e: + raise Exception(f"Failed to stage changes: {str(e)}") + + def get_commit_history(self, since: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, str]]: + """Get commit history. + + Args: + since: Get commits since this reference (tag, commit, etc.) + limit: Maximum number of commits to return + """ + args = [ + "log", + "--pretty=format:%H%n%an%n%ad%n%B%n==END==", + "--date=format:%Y-%m-%d %H:%M:%S %z" + ] + + if since: + args.append(f"{since}..HEAD") + if limit: + args.append(f"-n {limit}") + + stdout, _ = self._run_git_command(args) + + commits = [] + current_commit = [] + + for line in stdout.split('\n'): + if line == "==END==": + if current_commit: + commit_hash, author, date, *message_lines = current_commit + commits.append({ + 'hash': commit_hash, + 'author': author, + 'date': date, + 'message': '\n'.join(message_lines) + }) + current_commit = [] + else: + current_commit.append(line) + + return commits + + def get_commits_since_tag(self, tag: str) -> List[Dict[str, str]]: + """Get commits since a tag. + + Args: + tag: Git tag to use as reference point + + Returns: + List of commit dictionaries with hash and message + """ + cmd = ["git", "log", f"{tag}..HEAD", "--pretty=format:%H|%s"] + result = subprocess.run(cmd, capture_output=True, text=True, cwd=self.repo_path) + result.check_returncode() + + commits = [] + for line in result.stdout.splitlines(): + hash_, message = line.split("|", 1) + commits.append({"hash": hash_, "message": message}) + + return commits + + def get_commits_since_date(self, days: int) -> List[Dict[str, str]]: + """Get commits since a date. + + Args: + days: Number of days to look back + + Returns: + List of commit dictionaries with hash and message + """ + # Calculate date + date = datetime.now() - timedelta(days=days) + date_str = date.strftime("%Y-%m-%d") + + # Get commits + cmd = ["git", "log", f"--since={date_str}", "--pretty=format:%H|%s"] + result = subprocess.run(cmd, capture_output=True, text=True, cwd=self.repo_path) + result.check_returncode() + + commits = [] + for line in result.stdout.splitlines(): + hash_, message = line.split("|", 1) + commits.append({"hash": hash_, "message": message}) + + return commits \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3b846c6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.31.0 +setuptools>=61.0 +click>=8.0.0 +rich>=13.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2fccfea --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +""" +Setup script for devtools package. +""" + +from setuptools import setup, find_packages + +setup( + name="devtools", + version="0.1.0", + packages=find_packages(), + include_package_data=True, + install_requires=[ + "click>=8.0.0", + "rich>=10.0.0", + "requests>=2.25.0", + "pyyaml>=5.0.0", + ], + entry_points={ + "console_scripts": [ + "devtools=devtools.cli:cli", + ], + }, + author="Sankalp Tharu", + author_email="sankalptharu50028@gmail.com", + description="A collection of developer tools", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + url="https://github.com/S4NKALP/DevTools", + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: GPL License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + python_requires=">=3.7", +) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..7b020bb --- /dev/null +++ b/uv.lock @@ -0,0 +1,211 @@ +version = 1 +revision = 2 +requires-python = ">=3.7" +resolution-markers = [ + "python_full_version >= '3.9'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1b/81575c26402ec16ad3f779ef3d66099414180aae26681b3868e1cf27b9af/charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", size = 138020, upload-time = "2025-05-02T08:33:20.454Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/921f93af2c57f4aeb33e65a72ef012836b7663e9d96964cabb12786b9597/charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", size = 147223, upload-time = "2025-05-02T08:33:22.746Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/4b0eb8b7abdd0401babe6ce496473a8e1fb0c2ac7183f05333be030c5a3f/charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", size = 140280, upload-time = "2025-05-02T08:33:25.022Z" }, + { url = "https://files.pythonhosted.org/packages/38/84/cec08586c7c294e2c5e984de797c15b3efb30d42a0dd0132a62d2dfe3a27/charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", size = 141317, upload-time = "2025-05-02T08:33:26.793Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/c9a7455b44f7c9b95f3fb3ff768e395e6324692eb0eac8e30988d272d71a/charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", size = 143954, upload-time = "2025-05-02T08:33:29.105Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c8/4b308943a400cdefc678192bb027cd077dc1b015410394cb4dedc44b31f3/charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", size = 138731, upload-time = "2025-05-02T08:33:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/89/d6/54e374998ea75a58a886536273839aad3561c02e6db64defa3bda5478d4b/charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", size = 145728, upload-time = "2025-05-02T08:33:32.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/46/a0d87b114181e172ab3f5dbdf767e919b6ab9f38604c4fc6a2bf218566b5/charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", size = 149291, upload-time = "2025-05-02T08:33:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/6247f81a53824aa502cf39f05f9723fa9bdbbece51535de7146c51eb8270/charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", size = 148147, upload-time = "2025-05-02T08:33:36.643Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/b9cb1853ad3dd9fbbf8fb79b79e575b4a4f4a5a90acb6159a86a4d919a94/charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", size = 141843, upload-time = "2025-05-02T08:33:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b4/108e23067af86a699b10985299c2f959b6fb69b03d484ba40b5b40f917fe/charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", size = 96308, upload-time = "2025-05-02T08:33:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/29fad769c4cf1b171a4203799d9c3abcd0b552690c9437315a6a960751e7/charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", size = 103185, upload-time = "2025-05-02T08:33:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fd/f700cfd4ad876def96d2c769d8a32d808b12d1010b6003dc6639157f99ee/charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", size = 198257, upload-time = "2025-05-02T08:33:45.511Z" }, + { url = "https://files.pythonhosted.org/packages/3a/95/6eec4cbbbd119e6a402e3bfd16246785cc52ce64cf21af2ecdf7b3a08e91/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", size = 143453, upload-time = "2025-05-02T08:33:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/d4f913660383b3d93dbe6f687a312ea9f7e89879ae883c4e8942048174d4/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", size = 153130, upload-time = "2025-05-02T08:33:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/7540141529eabc55bf19cc05cd9b61c2078bebfcdbd3e799af99b777fc28/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", size = 145688, upload-time = "2025-05-02T08:33:52.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/d76d3d6e340fb0967c43c564101e28a78c9a363ea62f736a68af59ee3683/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", size = 147418, upload-time = "2025-05-02T08:33:54.718Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ef/b7c1f39c0dc3808160c8b72e0209c2479393966313bfebc833533cfff9cc/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", size = 150066, upload-time = "2025-05-02T08:33:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/4e47cc23d2a4a5eb6ed7d6f0f8cda87d753e2f8abc936d5cf5ad2aae8518/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", size = 144499, upload-time = "2025-05-02T08:33:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9c/efdf59dd46593cecad0548d36a702683a0bdc056793398a9cd1e1546ad21/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", size = 152954, upload-time = "2025-05-02T08:34:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/b3/4e8b73f7299d9aaabd7cd26db4a765f741b8e57df97b034bb8de15609002/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", size = 155876, upload-time = "2025-05-02T08:34:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/53/cb/6fa0ccf941a069adce3edb8a1e430bc80e4929f4d43b5140fdf8628bdf7d/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", size = 153186, upload-time = "2025-05-02T08:34:04.481Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c6/80b93fabc626b75b1665ffe405e28c3cef0aae9237c5c05f15955af4edd8/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", size = 148007, upload-time = "2025-05-02T08:34:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/41/eb/c7367ac326a2628e4f05b5c737c86fe4a8eb3ecc597a4243fc65720b3eeb/charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", size = 97923, upload-time = "2025-05-02T08:34:08.792Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/1c82646582ccf2c757fa6af69b1a3ea88744b8d2b4ab93b7686b2533e023/charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", size = 105020, upload-time = "2025-05-02T08:34:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "commit-gen" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "requests", version = "2.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.31.0" }] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "requests" +version = "2.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.8'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.8'" }, + { name = "idna", marker = "python_full_version < '3.8'" }, + { name = "urllib3", version = "2.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794, upload-time = "2023-05-22T15:12:44.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574, upload-time = "2023-05-22T15:12:42.313Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.8'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.8'" }, + { name = "idna", marker = "python_full_version >= '3.8'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "urllib3", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/af/47/b215df9f71b4fdba1025fc05a77db2ad243fa0926755a52c5e71659f4e3c/urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", size = 282546, upload-time = "2023-10-17T17:46:50.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/b2/b157855192a68541a91ba7b2bbcb91f1b4faa51f8bae38d8005c034be524/urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e", size = 124213, upload-time = "2023-10-17T17:46:48.538Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +]