initial
This commit is contained in:
23 files changed
+2117
No files matched your search
+48
@@ -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/
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Commit message generator module
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
DevTools - A collection of developer tools.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Changelog generation using AI.
|
||||
"""
|
||||
|
||||
from .generator import ChangelogGenerator
|
||||
|
||||
__all__ = [
|
||||
"ChangelogGenerator"
|
||||
]
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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]
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Gitignore generator module.
|
||||
"""
|
||||
|
||||
from .cli import cli
|
||||
from .generator import GitignoreGenerator
|
||||
from .detector import ProjectDetector
|
||||
|
||||
__all__ = [
|
||||
"cli",
|
||||
"GitignoreGenerator",
|
||||
"ProjectDetector"
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
requests>=2.31.0
|
||||
setuptools>=61.0
|
||||
click>=8.0.0
|
||||
rich>=13.0.0
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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" },
|
||||
]
|
||||
Reference in new issue
Block a user