Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ciftt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import typer

from cli import check_token, create_issues, export_issues, update_issues
from cli.create_comments import create_comments

app = typer.Typer(help="CIFTT - CSV Input for Feature Triage and Tracking")

app.command()(create_issues)
app.command()(create_comments)
app.command()(update_issues)
app.command()(export_issues)
app.command()(check_token)
Expand Down
46 changes: 23 additions & 23 deletions cli/common.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
"""
Common CLI utilities to eliminate code duplication across commands.
"""

from typing import Dict, List, Tuple

import typer

from cli.csv_data import load_and_validate_csv
from cli.github import init_github_client, validate_repository_access, validate_token_scopes
from cli.github import (
init_github_client,
validate_repository_access,
validate_token_scopes,
)
from csv_data import CSVData
from github.client import GitHubClient


def load_csv_for_command(csv_file: str) -> CSVData:
"""
Common CSV loading logic for all CLI commands.

Args:
csv_file: Path to the CSV file

Returns:
CSVData object with loaded and validated data
"""
Expand All @@ -28,38 +33,37 @@ def load_csv_for_command(csv_file: str) -> CSVData:


def setup_github_client_for_command(
required_scopes: List[str],
repositories: List[Tuple[str, str]] = None
required_scopes: List[str], repositories: List[Tuple[str, str]] = None
) -> GitHubClient:
"""
Initialize and validate GitHub client for CLI commands.

Args:
required_scopes: List of required OAuth scopes
repositories: Optional list of (owner, repo) tuples to validate access

Returns:
Initialized and validated GitHubClient
"""
github_client = init_github_client()

validate_token_scopes(github_client, required_scopes)

if repositories:
for owner, repo_name in repositories:
validate_repository_access(github_client, owner, repo_name)

return github_client


def handle_cli_error(operation_name: str, exception: Exception) -> None:
"""
Standard error handling for CLI operations.

Args:
operation_name: Name of the operation that failed
exception: The exception that was raised

Raises:
typer.Exit: Always exits with code 1
"""
Expand All @@ -71,23 +75,23 @@ def validate_project_fields_for_csv(
csv_data: CSVData,
github_client: GitHubClient,
project_owner: str,
project_number: str
project_number: str,
) -> None:
"""
Validate that CSV project fields exist in the GitHub project.

Args:
csv_data: CSV data containing project fields
github_client: GitHub client for API calls
project_owner: Project owner (user or organization)
project_number: Project number

Raises:
typer.Exit: If validation fails
"""
if not csv_data.has_project_fields():
return

try:
field_definitions = github_client.get_project_field_definitions(
project_owner, project_number
Expand All @@ -104,16 +108,12 @@ def validate_project_fields_for_csv(
invalid_fields.append(csv_field)

if invalid_fields:
typer.echo(
f"❌ Invalid project fields found: {', '.join(invalid_fields)}"
)
typer.echo(f"❌ Invalid project fields found: {', '.join(invalid_fields)}")
available_fields = list(field_definitions.keys())
typer.echo(
f"📋 Available project fields: {', '.join(available_fields)}"
)
typer.echo(f"📋 Available project fields: {', '.join(available_fields)}")
raise typer.Exit(code=1)
else:
typer.echo(f"✅ Project fields validated: {', '.join(valid_fields)}")

except ValueError as e:
handle_cli_error("Project field validation", e)
handle_cli_error("Project field validation", e)
177 changes: 177 additions & 0 deletions cli/create_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import typer

from utils import extract_issue_number, extract_repo_from_issue_url

from .common import (
handle_cli_error,
load_csv_for_command,
setup_github_client_for_command,
)


def create_comments(
csv_file: str = typer.Argument(
..., help="Path to the CSV file containing comment data"
),
dry_run: bool = typer.Option(
False, "--dry-run", "-d", help="Print actions without executing them"
),
):
"""
Create comments on existing GitHub issues from a CSV file.

CSV format should include URL and Comment columns.
Optional columns: Author, Date for attribution.
"""
try:
csv_data = load_csv_for_command(csv_file)

_validate_csv_format(csv_data)

if dry_run:
_perform_dry_run(csv_data)
return

github_client = setup_github_client_for_command(required_scopes=["repo"])

_create_comments_from_csv(github_client, csv_data)

except Exception as e:
handle_cli_error("Comment creation", e)


def _validate_csv_format(csv_data) -> None:
"""
Validate that the CSV has required columns for comment creation.

Args:
csv_data: CSVData object to validate

Raises:
ValueError: If required columns are missing
"""
if "URL" not in csv_data.data.columns:
raise ValueError("CSV must contain 'URL' column")

if "Comment" not in csv_data.data.columns:
raise ValueError("CSV must contain 'Comment' column")

# Check for empty URLs or comments
empty_urls = csv_data.data["URL"].isna() | (csv_data.data["URL"] == "")
if empty_urls.any():
empty_rows = list(csv_data.data.index[empty_urls] + 1)
raise ValueError(f"Empty URL values found in rows: {empty_rows}")

empty_comments = csv_data.data["Comment"].isna() | (csv_data.data["Comment"] == "")
if empty_comments.any():
empty_rows = list(csv_data.data.index[empty_comments] + 1)
raise ValueError(f"Empty Comment values found in rows: {empty_rows}")


def _perform_dry_run(csv_data) -> None:
"""
Perform a dry run showing what comments would be created.

Args:
csv_data: CSVData object with comment data
"""
typer.echo("🧪 Dry run mode - showing what would be done:")
typer.echo()

for index, row in csv_data.data.iterrows():
try:
url = row["URL"]
owner, repo_name = extract_repo_from_issue_url(url)
issue_number = extract_issue_number(url)

# Format comment body with attribution if provided
formatted_comment = _format_comment_body(row)

typer.echo(f"📝 Would create comment on {owner}/{repo_name}#{issue_number}")
typer.echo(
f" Comment: {formatted_comment[:100]}{'...' if len(formatted_comment) > 100 else ''}"
)
typer.echo()

except Exception as e:
typer.echo(f"❌ Row {index + 1}: Invalid URL format - {e}")
typer.echo()


def _create_comments_from_csv(github_client, csv_data) -> None:
"""
Create comments from CSV data.

Args:
github_client: GitHub client for API calls
csv_data: CSVData object with comment data
"""
typer.echo(f"💬 Creating comments from {len(csv_data.data)} rows...")
typer.echo()

success_count = 0
error_count = 0

for index, row in csv_data.data.iterrows():
try:
url = row["URL"]
owner, repo_name = extract_repo_from_issue_url(url)
issue_number = extract_issue_number(url)

formatted_comment = _format_comment_body(row)

# Create the comment
response = github_client.create_issue_comment(
owner, repo_name, issue_number, formatted_comment
)

comment_id = response.get("id")
typer.echo(
f"✅ Created comment {comment_id} on {owner}/{repo_name}#{issue_number}"
)
success_count += 1

except Exception as e:
typer.echo(f"❌ Row {index + 1}: Failed to create comment - {e}")
error_count += 1

typer.echo()
typer.echo(f"📊 Summary: {success_count} comments created, {error_count} failed")


def _format_comment_body(row) -> str:
"""
Format comment body with optional author attribution.

Args:
row: Pandas Series with comment data

Returns:
Formatted comment body string
"""
comment = str(row["Comment"]).strip()

# Add attribution if Author or Date columns are present
attribution_parts = []

if (
"Author" in row
and row["Author"] is not None
and str(row["Author"]).strip()
and str(row["Author"]).lower() != "nan"
):
attribution_parts.append(f"Originally by: {str(row['Author']).strip()}")

if (
"Date" in row
and row["Date"] is not None
and str(row["Date"]).strip()
and str(row["Date"]).lower() != "nan"
):
attribution_parts.append(f"Date: {str(row['Date']).strip()}")

if attribution_parts:
attribution = " | ".join(attribution_parts)
comment = f"{comment}\n\n*{attribution}*"

return comment
3 changes: 1 addition & 2 deletions cli/create_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ def create_issues(
return

github_client = setup_github_client_for_command(
required_scopes=["repo"],
repositories=[(owner, repo_name)]
required_scopes=["repo"], repositories=[(owner, repo_name)]
)

issues = transform_csv_to_new_issues(csv_data.data)
Expand Down
18 changes: 11 additions & 7 deletions cli/update_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
from transform import transform_csv_to_updated_issues
from utils import extract_repo_from_issue_url, parse_github_project_identifier

from .common import handle_cli_error, load_csv_for_command, setup_github_client_for_command, validate_project_fields_for_csv
from .common import (
handle_cli_error,
load_csv_for_command,
setup_github_client_for_command,
validate_project_fields_for_csv,
)
from .dry_run import perform_dry_run
from .issues import update_issues_in_github

Expand Down Expand Up @@ -98,23 +103,22 @@ def update_issues(
return

github_client = setup_github_client_for_command(
required_scopes=["repo", "project"],
repositories=repositories
required_scopes=["repo", "project"], repositories=repositories
)

# Validate that the project exists and is accessible
try:
project_info = github_client.validate_project_exists(
project_owner, project_number
)
typer.echo(
f"✅ Project validated: {project_info.title} ({project_info.type})"
)
typer.echo(f"✅ Project validated: {project_info.title} ({project_info.type})")
except ValueError as e:
handle_cli_error("Project validation", e)

# Validate project fields before processing issues
validate_project_fields_for_csv(csv_data, github_client, project_owner, project_number)
validate_project_fields_for_csv(
csv_data, github_client, project_owner, project_number
)

# Repository access already validated by setup_github_client_for_command

Expand Down
Loading