This commit is contained in:
sankalp committed 2025-06-04 09:39:28 +05:45
1 parent 61849e7645
commit 1814fdf298
23 files changed
+2117

No files matched your search

+48
View File
@@ -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/
+241
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
"""
Commit message generator module
"""
__version__ = "0.1.0"
+5
View File
@@ -0,0 +1,5 @@
"""
DevTools - A collection of developer tools.
"""
__version__ = "0.1.0"
+9
View File
@@ -0,0 +1,9 @@
"""
Changelog generation using AI.
"""
from .generator import ChangelogGenerator
__all__ = [
"ChangelogGenerator"
]
+111
View File
@@ -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")
+139
View File
@@ -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
+77
View File
@@ -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()
+15
View File
@@ -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"
]
+247
View File
@@ -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()
+79
View File
@@ -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
)
+41
View File
@@ -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]
+13
View File
@@ -0,0 +1,13 @@
"""
Gitignore generator module.
"""
from .cli import cli
from .generator import GitignoreGenerator
from .detector import ProjectDetector
__all__ = [
"cli",
"GitignoreGenerator",
"ProjectDetector"
]
+84
View File
@@ -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()
+161
View File
@@ -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
+106
View File
@@ -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)
+9
View File
@@ -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"]
+107
View File
@@ -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
+134
View File
@@ -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()
+231
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
requests>=2.31.0
setuptools>=61.0
click>=8.0.0
rich>=13.0.0
+40
View File
@@ -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",
)
Generated
+211
View File
@@ -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" },
]