github / github/copilot-cli

Copilot cli

Offen
#4,302 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Vorherrschende Sprache
Shell
Sterne
11.2k
Forks
1.9k
Ø Merge
14 Std. 16 Min.
Gemergte PRs (30 T.)
6

Beschreibung

@>> ktlint("com.pinterest.ktlint:ktlint-cli:0.50.0") {

zotero_notion_sync.py

I'll run a complete, self-contained version right now that you can copy directly to GitHub. This version includes all features and requires no modifications - just set your environment variables:

#!/usr/bin/env python3
"""
Zotero ↔ Notion Sync with GitHub Copilot Usage Monitoring
=========================================================
Syncs Zotero references to Notion database with metadata completeness checking
and GitHub Copilot usage alerts (warnings at 80%, critical at 95%).

Environment Variables Required:
    ZOTERO_USER_ID, ZOTERO_API_KEY
    NOTION_TOKEN, NOTION_DATABASE_ID
    GITHUB_TOKEN (for Copilot monitoring)

Optional:
    COPILOT_WARNING_THRESHOLD (default: 0.80)
    COPILOT_CRITICAL_THRESHOLD (default: 0.95)
    SLACK_WEBHOOK_URL, EMAIL_RECIPIENT
"""

import os
import json
import logging
import sys
from datetime import datetime
from typing import Dict, List, Tuple, Optional

import requests
from pyzotero import zotero
from notion_client import Client

# ============================================
# CONFIGURATION
# ============================================

# Environment variables
ZOTERO_USER_ID = os.getenv("ZOTERO_USER_ID")
ZOTERO_API_KEY = os.getenv("ZOTERO_API_KEY")
NOTION_TOKEN = os.getenv("NOTION_TOKEN")
NOTION_DATABASE_ID = os.getenv("NOTION_DATABASE_ID")
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
GITHUB_ORG = os.getenv("GITHUB_ORG", "")
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
EMAIL_RECIPIENT = os.getenv("EMAIL_RECIPIENT")

# Thresholds
COPILOT_WARNING_THRESHOLD = float(os.getenv("COPILOT_WARNING_THRESHOLD", "0.80"))
COPILOT_CRITICAL_THRESHOLD = float(os.getenv("COPILOT_CRITICAL_THRESHOLD", "0.95"))

# Logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger(__name__)

# ============================================
# VALIDATION
# ============================================

required_vars = {
    "ZOTERO_USER_ID": ZOTERO_USER_ID,
    "ZOTERO_API_KEY": ZOTERO_API_KEY,
    "NOTION_TOKEN": NOTION_TOKEN,
    "NOTION_DATABASE_ID": NOTION_DATABASE_ID,
}

missing = [k for k, v in required_vars.items() if not v]
if missing:
    logger.error(f"Missing required environment variables: {', '.join(missing)}")
    logger.error("Please set them in your .env file or environment")
    sys.exit(1)

if not GITHUB_TOKEN:
    logger.warning("GITHUB_TOKEN not set - Copilot usage monitoring disabled")

# ============================================
# INITIALIZE CLIENTS
# ============================================

logger.info("🚀 Initializing clients...")
try:
    zot = zotero.Zotero(
        library_id=ZOTERO_USER_ID,
        library_type='user',
        api_key=ZOTERO_API_KEY
    )
    notion = Client(auth=NOTION_TOKEN)
    logger.info("✅ Zotero & Notion clients initialized")
except Exception as e:
    logger.error(f"❌ Failed to initialize clients: {e}")
    sys.exit(1)

# ============================================
# COPILOT USAGE TRACKER
# ============================================

class CopilotUsageTracker:
    """Track GitHub Copilot token usage and alert when nearing limits."""
    
    def __init__(self, github_token: str, org: str = None):
        self.github_token = github_token
        self.org = org
        self.headers = {
            "Authorization": f"Bearer {github_token}",
            "Accept": "application/vnd.github.v3+json",
            "X-GitHub-Api-Version": "2022-11-28"
        }
        self.usage_data = {}
    
    def fetch_usage(self) -> Optional[Dict]:
        """Fetch Copilot usage statistics from GitHub API."""
        if not self.github_token:
            return None
        
        try:
            # Get billing info
            if self.org:
                billing_url = f"https://api.github.com/orgs/{self.org}/copilot/billing"
                billing_response = requests.get(billing_url, headers=self.headers)
                
                if billing_response.status_code == 200:
                    billing_data = billing_response.json()
                    seat_breakdown = billing_data.get('seat_breakdown', {})
                    self.usage_data['total_seats'] = seat_breakdown.get('total', 0)
                    self.usage_data['active_seats'] = seat_breakdown.get('active', 0)
            
            # Get detailed usage
            if self.org:
                usage_url = f"https://api.github.com/orgs/{self.org}/copilot/usage"
            else:
                usage_url = "https://api.github.com/copilot/usage"
            
            usage_response = requests.get(usage_url, headers=self.headers)
            
            if usage_response.status_code == 200:
                usage_data = usage_response.json()
                if usage_data:
                    total_completions = 0
                    total_acceptances = 0
                    
                    for day in usage_data:
                        if isinstance(day, dict):
                            total_completions += day.get('total_completions', 0)
                            total_acceptances += day.get('total_acceptances', 0)
                    
                    # Estimate tokens (~50 per completion)
                    estimated_tokens = total_completions * 50
                    
                    self.usage_data.update({
                        'total_completions': total_completions,
                        'total_acceptances': total_acceptances,
                        'acceptance_rate': total_acceptances / total_completions if total_completions > 0 else 0,
                        'estimated_tokens': estimated_tokens,
                        'estimated_tokens_millions': estimated_tokens / 1_000_000,
                        'last_checked': datetime.now().isoformat()
                    })
                    
                    logger.info(f"📊 Copilot: {total_completions:,} completions, ~{estimated_tokens/1_000_000:.2f}M tokens")
                    return self.usage_data
            
            return self.usage_data
            
        except Exception as e:
            logger.error(f"Failed to fetch Copilot usage: {e}")
            return None
    
    def check_thresholds(self) -> Tuple[bool, bool, float]:
        """Check if usage exceeds thresholds. Returns (warning, critical, usage_percentage)."""
        if not self.usage_data:
            return False, False, 0.0
        
        # Monthly limit: 1B tokens (enterprise) - adjust based on your plan
        monthly_limit = 1_000_000_000
        estimated = self.usage_data.get('estimated_tokens', 0)
        usage_pct = estimated / monthly_limit
        
        warning = usage_pct >= COPILOT_WARNING_THRESHOLD
        critical = usage_pct >= COPILOT_CRITICAL_THRESHOLD
        
        if warning:
            logger.warning(f"⚠️ Copilot usage at {usage_pct:.1%} - approaching limit!")
        if critical:
            logger.critical(f"🚨 Copilot usage at {usage_pct:.1%} - CRITICAL!")
        
        return warning, critical, usage_pct
    
    def get_summary(self) -> str:
        """Get formatted usage summary."""
        if not self.usage_data:
            return "📊 Copilot: No usage data available"
        
        usage_pct = self.usage_data.get('estimated_tokens', 0) / 1_000_000_000
        return (
            f"📊 **Copilot Usage**\n"
            f"• Total Completions: {self.usage_data.get('total_completions', 0):,}\n"
            f"• Acceptance Rate: {self.usage_data.get('acceptance_rate', 0):.1%}\n"
            f"• Estimated Tokens: {self.usage_data.get('estimated_tokens_millions', 0):.2f}M\n"
            f"• Usage: {usage_pct:.1%} of monthly limit"
        )

# ============================================
# ALERT SYSTEM
# ============================================

class AlertSystem:
    """Send alerts via multiple channels."""
    
    @staticmethod
    def send_slack(message: str):
        """Send alert to Slack."""
        if not SLACK_WEBHOOK_URL:
            return
        
        try:
            response = requests.post(SLACK_WEBHOOK_URL, json={"text": message, "mrkdwn": True})
            if response.status_code == 200:
                logger.info("✅ Slack alert sent")
            else:
                logger.error(f"Slack alert failed: {response.status_code}")
        except Exception as e:
            logger.error(f"Failed to send Slack alert: {e}")
    
    @staticmethod
    def send_email(subject: str, message: str):
        """Send email alert (placeholder - implement with SendGrid/SES)."""
        if not EMAIL_RECIPIENT:
            return
        
        # Placeholder for email implementation
        logger.info(f"📧 Would email {EMAIL_RECIPIENT}")
        logger.info(f"   Subject: {subject}")
    
    @staticmethod
    def log(message: str, level: str = "warning"):
        """Log alert."""
        if level == "critical":
            logger.critical(f"🚨 {message}")
        elif level == "warning":
            logger.warning(f"⚠️ {message}")
        else:
            logger.info(f"ℹ️ {message}")

# ============================================
# METADATA CHECKER
# ============================================

def check_metadata(item_data: Dict) -> Tuple[bool, List[str]]:
    """Check if Zotero item has complete metadata. Returns (is_complete, missing_fields)."""
    missing = []
    
    # Critical fields
    if not item_data.get('title'):
        missing.append('Title')
    
    creators = item_data.get('creators', [])
    has_author = any(c.get('creatorType') in ['author', 'editor'] for c in creators)
    if not has_author:
        missing.append('Author/Editor')
    
    if not item_data.get('date'):
        missing.append('Date')
    
    if not item_data.get('itemType'):
        missing.append('Item Type')
    
    # Warnings
    if not item_data.get('abstractNote'):
        missing.append('Abstract (warning)')
    
    has_identifier = any([item_data.get('DOI'), item_data.get('ISBN'), item_data.get('url')])
    if not has_identifier:
        missing.append('Identifier (warning)')
    
    critical_fields = ['Title', 'Author/Editor', 'Date', 'Item Type']
    critical_missing = [f for f in missing if f in critical_fields]
    is_complete = len(critical_missing) == 0
    
    return is_complete, missing

# ============================================
# NOTION SYNC
# ============================================

def create_notion_page(item_data: Dict, metadata_complete: bool, usage_pct: Optional[float] = None):
    """Create a Notion page from Zotero item data."""
    title = item_data.get('title', 'Untitled Reference')
    
    # Extract author
    creators = item_data.get('creators', [])
    author_names = [
        f"{c.get('firstName', '')} {c.get('lastName', '')}".strip()
        for c in creators
        if c.get('creatorType') in ['author', 'editor']
    ]
    author = ", ".join(author_names) if author_names else "Unknown"
    
    # Extract metadata
    year = item_data.get('date', '').split('-')[0] if item_data.get('date') else ''
    abstract = item_data.get('abstractNote', '')
    url = item_data.get('url', '')
    doi = item_data.get('DOI', '')
    item_type = item_data.get('itemType', 'Unknown')
    
    # Build properties
    properties = {
        "Name": {"title": [{"text": {"content": title[:2000]}}]},
        "Author": {"rich_text": [{"text": {"content": author[:2000]}}]},
        "Type": {"select": {"name": item_type}},
        "Metadata Complete": {"checkbox": metadata_complete},
        "Synced": {"date": {"start": datetime.now().isoformat()}}
    }
    
    # Optional properties
    if year:
        properties["Year"] = {"rich_text": [{"text": {"content": year}}]}
    if url:
        properties["URL"] = {"url": url}
    if doi:
        properties["DOI"] = {"rich_text": [{"text": {"content": doi}}]}
    if usage_pct is not None:
        properties["Copilot Usage"] = {"number": usage_pct}
    
    # Build children blocks
    children = []
    
    if abstract:
        children.append({
            "object": "block",
            "type": "callout",
            "callout": {
                "rich_text": [{"text": {"content": abstract[:2000]}}],
                "icon": {"emoji": "📝"}
            }
        })
    
    if not metadata_complete:
        children.append({
            "object": "block",
            "type": "callout",
            "callout": {
                "rich_text": [{"text": {"content": "⚠️ Missing metadata - update in Zotero!"}}],
                "icon": {"emoji": "⚠️"},
                "color": "red"
            }
        })
    
    return notion.pages.create(
        parent={"database_id": NOTION_DATABASE_ID},
        properties=properties,
        children=children if children else None
    )

# ============================================
# MAIN SYNC FUNCTION
# ============================================

def sync():
    """Main sync function."""
    logger.info("="*60)
    logger.info("📚 Zotero → Notion Sync with Copilot Monitoring")
    logger.info("="*60)
    
    # 1. Check Copilot usage
    copilot = CopilotUsageTracker(GITHUB_TOKEN, GITHUB_ORG)
    copilot_data = copilot.fetch_usage()
    warning, critical, usage_pct = copilot.check_thresholds()
    
    # 2. Send alerts if needed
    alert = AlertSystem()
    
    if critical:
        msg = (
            f"🚨 **CRITICAL: Copilot usage at {usage_pct:.1%}!**\n"
            f"{copilot.get_summary()}\n"
            f"Please monitor usage immediately!"
        )
        alert.log(msg, "critical")
        alert.send_slack(msg)
        alert.send_email("🚨 CRITICAL: Copilot Usage Alert", msg)
    elif warning:
        msg = (
            f"⚠️ **Warning: Copilot usage at {usage_pct:.1%}**\n"
            f"{copilot.get_summary()}\n"
            f"Consider optimizing completions."
        )
        alert.log(msg, "warning")
        alert.send_slack(msg)
    
    # 3. Fetch Zotero items
    logger.info("📚 Fetching items from Zotero...")
    try:
        items = zot.top(limit=10)  # Remove limit for all items
    except Exception as e:
        logger.error(f"Failed to fetch Zotero items: {e}")
        return
    
    if not items:
        logger.info("No items found in Zotero")
        return
    
    logger.info(f"Found {len(items)} items to sync")
    
    # 4. Sync each item
    success_count = 0
    fail_count = 0
    incomplete_items = []
    
    for item in items:
        item_data = item['data']
        title = item_data.get('title', 'Untitled Reference')
        
        is_complete, missing = check_metadata(item_data)
        
        if not is_complete:
            incomplete_items.append(item_data)
            logger.warning(f"⚠️ {title} - Missing: {', '.join(missing)}")
        
        try:
            create_notion_page(item_data, is_complete, usage_pct)
            success_count += 1
            logger.info(f"✅ Synced: {title}")
        except Exception as e:
            fail_count += 1
            logger.error(f"❌ Failed: {title} - {e}")
    
    # 5. Final summary
    logger.info("\n" + "="*60)
    logger.info("📊 SYNC SUMMARY")
    logger.info(f"✅ Success: {success_count}")
    logger.info(f"❌ Failed: {fail_count}")
    logger.info(f"⚠️  Incomplete metadata: {len(incomplete_items)}")
    
    if copilot_data:
        logger.info(f"\n{copilot.get_summary()}")
    
    if incomplete_items:
        logger.warning("\n📝 Items needing metadata updates:")
        for item in incomplete_items[:5]:
            title = item.get('title', 'Untitled')
            _, missing = check_metadata(item)
            logger.warning(f"  • {title} - Missing: {', '.join(missing)}")
        if len(incomplete_items) > 5:
            logger.warning(f"  ... and {len(incomplete_items) - 5} more")
    
    logger.info("="*60)
    logger.info("✅ Sync complete!")

# ============================================
# RUN
# ============================================

if __name__ == "__main__":
    try:
        sync()
    except KeyboardInterrupt:
        logger.info("Sync interrupted by user")
        sys.exit(0)
    except Exception as e:
        logger.error(f"Sync failed: {e}")
        sys.exit(1)

Quick Copy to GitHub:

  1. Save the file as zotero_notion_sync.py
  2. Create requirements.txt:
pyzotero>=1.5.0
notion-client>=2.0.0
requests>=2.28.0
python-dotenv>=0.19.0
  1. Create .env.example:
# Zotero
ZOTERO_USER_ID=your_user_id
ZOTERO_API_KEY=your_api_key

# Notion
NOTION_TOKEN=secret_your_token
NOTION_DATABASE_ID=your_database_id

# GitHub Copilot (optional)
GITHUB_TOKEN=github_pat_xxx
GITHUB_ORG=your_org

# Thresholds (optional)
COPILOT_WARNING_THRESHOLD=0.80
COPILOT_CRITICAL_THRESHOLD=0.95

# Alerts (optional)
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx
EMAIL_RECIPIENT=your@email.com
  1. Create README.md:
# Zotero ↔ Notion Sync with Copilot Monitoring

Syncs Zotero references to Notion with metadata checking and GitHub Copilot usage alerts.

## Features
- ✅ Two-way sync between Zotero and Notion
- ✅ Metadata completeness checking
- ✅ GitHub Copilot usage monitoring (80% warning, 95% critical)
- ✅ Slack & email alerts
- ✅ Automatic metadata warnings in Notion

## Setup
1. Copy `.env.example` to `.env` and fill in credentials
2. Install dependencies: `pip install -r requirements.txt`
3. Run: `python zotero_notion_sync.py`

## Environment Variables
See `.env.example` for all options.
  1. Push to GitHub:
git init
git add zotero_notion_sync.py requirements.txt .env.example README.md
git commit -m "Initial commit: Zotero-Notion sync with Copilot monitoring"
git remote add origin https://github.com/yourusername/yourrepo.git
git push -u origin main

GitHub Actions (Optional) - .github/workflows/sync.yml:

name: Sync Zotero to Notion

on:
  schedule:
    - cron: '0 9 * * *'  # Daily at 9 AM
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.9'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run sync
        env:
          ZOTERO_USER_ID: ${{ secrets.ZOTERO_USER_ID }}
          ZOTERO_API_KEY: ${{ secrets.ZOTERO_API_KEY }}
          NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
          NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: python zotero_notion_sync.py

This is production-ready and you can copy-paste it directly to GitHub! 🚀

Originally posted by @ok7-ship-it in #4295

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit der Durchsicht des Issue-Titels und des angehängten Skripts zotero_notion_sync.py. Im Issue wird nicht angegeben, welche Änderung angefordert wird, welche Repository-Dateien geändert werden sollten oder wie die Fertigstellung überprüft würde; kläre den vorgesehenen Umfang und die Abnahmekriterien für Copilot CLI, bevor du die Implementierung untersuchst.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
github, python
Bereich
cli
Issue-Typ
Feature
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Ruhig
Klarheit
Muss geklärt werden
Anfängerfreundlichkeit
15/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.