chore: cleanup unused scripts and fix misplaced test file (#17611)
Remove scripts/ directory containing unused development/debug scripts: - mock_ibm_guardrails_server.py - test_groq_streaming_issue.py (debug for #12660) - test_mock_ibm_guardrails.py - update_readme_providers_table.py Move misplaced test file to correct location: - test_litellm/ -> tests/test_litellm/ (from PR #17221)
This commit is contained in:
parent
b673177b22
commit
a7ad8a36a4
@ -1,358 +0,0 @@
|
||||
"""
|
||||
Mock FastAPI server for IBM FMS Guardrails Orchestrator Detector API.
|
||||
|
||||
This server implements the Detector API endpoints for testing purposes.
|
||||
Based on: https://foundation-model-stack.github.io/fms-guardrails-orchestrator/
|
||||
|
||||
Usage:
|
||||
python scripts/mock_ibm_guardrails_server.py
|
||||
|
||||
The server will run on http://localhost:8001 by default.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Header, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="IBM FMS Guardrails Orchestrator Mock",
|
||||
description="Mock server for testing IBM Guardrails Detector API",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
|
||||
# Request Models
|
||||
class DetectorParams(BaseModel):
|
||||
"""Parameters specific to the detector."""
|
||||
|
||||
threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
|
||||
custom_param: Optional[str] = None
|
||||
|
||||
|
||||
class TextDetectionRequest(BaseModel):
|
||||
"""Request model for text detection."""
|
||||
|
||||
contents: List[str] = Field(..., description="Text content to analyze")
|
||||
detector_params: Optional[DetectorParams] = None
|
||||
|
||||
|
||||
class TextGenerationDetectionRequest(BaseModel):
|
||||
"""Request model for text generation detection."""
|
||||
|
||||
detector_id: str = Field(..., description="ID of the detector to use")
|
||||
prompt: str = Field(..., description="Input prompt")
|
||||
generated_text: str = Field(..., description="Generated text to analyze")
|
||||
detector_params: Optional[DetectorParams] = None
|
||||
|
||||
|
||||
class ContextDetectionRequest(BaseModel):
|
||||
"""Request model for detection with context."""
|
||||
|
||||
detector_id: str = Field(..., description="ID of the detector to use")
|
||||
content: str = Field(..., description="Text content to analyze")
|
||||
context: Optional[Dict[str, Any]] = Field(None, description="Additional context")
|
||||
detector_params: Optional[DetectorParams] = None
|
||||
|
||||
|
||||
# Response Models
|
||||
class Detection(BaseModel):
|
||||
"""Individual detection result."""
|
||||
|
||||
detection_type: str = Field(..., description="Type of detection")
|
||||
detection: bool = Field(..., description="Whether content was detected as harmful")
|
||||
score: float = Field(..., ge=0.0, le=1.0, description="Detection confidence score")
|
||||
start: Optional[int] = Field(None, description="Start position in text")
|
||||
end: Optional[int] = Field(None, description="End position in text")
|
||||
text: Optional[str] = Field(None, description="Detected text segment")
|
||||
evidence: Optional[List[str]] = Field(None, description="Supporting evidence")
|
||||
|
||||
|
||||
class DetectionResponse(BaseModel):
|
||||
"""Response model for detection results."""
|
||||
|
||||
detections: List[Detection] = Field(..., description="List of detections")
|
||||
detection_id: str = Field(..., description="Unique ID for this detection request")
|
||||
|
||||
|
||||
# Mock detector configurations
|
||||
MOCK_DETECTORS = {
|
||||
"hate": {
|
||||
"name": "Hate Speech Detector",
|
||||
"triggers": ["hate", "offensive", "discriminatory", "slur"],
|
||||
"default_score": 0.85,
|
||||
},
|
||||
"pii": {
|
||||
"name": "PII Detector",
|
||||
"triggers": ["email", "ssn", "credit card", "phone number", "address"],
|
||||
"default_score": 0.92,
|
||||
},
|
||||
"toxicity": {
|
||||
"name": "Toxicity Detector",
|
||||
"triggers": ["toxic", "abusive", "profanity", "insult"],
|
||||
"default_score": 0.78,
|
||||
},
|
||||
"jailbreak": {
|
||||
"name": "Jailbreak Detector",
|
||||
"triggers": ["ignore instructions", "override", "bypass", "jailbreak"],
|
||||
"default_score": 0.88,
|
||||
},
|
||||
"prompt_injection": {
|
||||
"name": "Prompt Injection Detector",
|
||||
"triggers": ["ignore previous", "new instructions", "system prompt"],
|
||||
"default_score": 0.90,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def simulate_detection(
|
||||
detector_id: str, content: str, detector_params: Optional[DetectorParams] = None
|
||||
) -> List[Detection]:
|
||||
"""
|
||||
Simulate detection logic based on detector type and content.
|
||||
|
||||
Args:
|
||||
detector_id: ID of the detector to simulate
|
||||
content: Text content to analyze
|
||||
detector_params: Optional detector parameters
|
||||
|
||||
Returns:
|
||||
List of Detection objects
|
||||
"""
|
||||
detections = []
|
||||
content_lower = " ".join(c for c in content).lower()
|
||||
|
||||
# Get detector config
|
||||
detector_config = MOCK_DETECTORS.get(detector_id)
|
||||
if not detector_config:
|
||||
# Unknown detector - return no detections
|
||||
return detections
|
||||
|
||||
# Check for triggers in content
|
||||
for trigger in detector_config["triggers"]:
|
||||
if trigger in content_lower:
|
||||
# Calculate score (use threshold if provided, otherwise default)
|
||||
base_score = detector_config["default_score"]
|
||||
threshold = (
|
||||
detector_params.threshold
|
||||
if detector_params and detector_params.threshold
|
||||
else None
|
||||
)
|
||||
|
||||
# Adjust score slightly based on content length (longer content = slightly lower confidence)
|
||||
score_adjustment = max(0, min(0.1, len(content) / 10000))
|
||||
score = max(0.0, min(1.0, base_score - score_adjustment))
|
||||
|
||||
# Find position of trigger
|
||||
start_pos = content_lower.find(trigger)
|
||||
end_pos = start_pos + len(trigger)
|
||||
|
||||
detection = Detection(
|
||||
detection_type=detector_id,
|
||||
detection=threshold is None or score >= threshold,
|
||||
score=score,
|
||||
start=start_pos,
|
||||
end=end_pos,
|
||||
text=content[start_pos:end_pos] if start_pos >= 0 else None,
|
||||
evidence=[f"Found trigger word: {trigger}"],
|
||||
)
|
||||
detections.append(detection)
|
||||
|
||||
# If no triggers found, return a negative detection
|
||||
if not detections:
|
||||
detections.append(
|
||||
Detection(
|
||||
detection_type=detector_id,
|
||||
detection=False,
|
||||
score=0.05, # Low score for clean content
|
||||
)
|
||||
)
|
||||
|
||||
return detections
|
||||
|
||||
|
||||
# Authentication middleware
|
||||
def verify_auth_token(authorization: Optional[str] = Header(None)) -> bool:
|
||||
"""
|
||||
Verify the authentication token.
|
||||
|
||||
Args:
|
||||
authorization: Authorization header value
|
||||
|
||||
Returns:
|
||||
True if valid, raises HTTPException otherwise
|
||||
"""
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authorization header",
|
||||
)
|
||||
|
||||
# Simple token validation - in real implementation, this would validate against a real auth system
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authorization header format. Expected: Bearer <token>",
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
|
||||
# Accept any non-empty token for mock purposes
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Empty token provided",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# API Endpoints
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy", "service": "IBM FMS Guardrails Mock Server"}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint with API information."""
|
||||
return {
|
||||
"service": "IBM FMS Guardrails Orchestrator Mock",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"text_detection": "/api/v1/text/detection",
|
||||
"generation_detection": "/api/v1/text/generation/detection",
|
||||
"context_detection": "/api/v1/text/context/detection",
|
||||
},
|
||||
"available_detectors": list(MOCK_DETECTORS.keys()),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/v1/text/contents")
|
||||
async def text_detection(
|
||||
request: TextDetectionRequest,
|
||||
detector_id: str = Header(None), # query parameter
|
||||
authorization: Optional[str] = Header(None),
|
||||
):
|
||||
"""
|
||||
Detect potential issues in text content.
|
||||
|
||||
Args:
|
||||
request: Detection request with content and detector ID
|
||||
detector_id: ID of detector
|
||||
authorization: Bearer token for authentication
|
||||
|
||||
Returns:
|
||||
Detection results
|
||||
"""
|
||||
verify_auth_token(authorization)
|
||||
|
||||
detections = simulate_detection(
|
||||
detector_id=detector_id,
|
||||
content=request.contents,
|
||||
detector_params=request.detector_params,
|
||||
)
|
||||
|
||||
return detections
|
||||
|
||||
|
||||
@app.post("/api/v1/text/generation/detection", response_model=DetectionResponse)
|
||||
async def text_generation_detection(
|
||||
request: TextGenerationDetectionRequest,
|
||||
authorization: Optional[str] = Header(None),
|
||||
):
|
||||
"""
|
||||
Detect potential issues in generated text.
|
||||
|
||||
Args:
|
||||
request: Detection request with prompt and generated text
|
||||
authorization: Bearer token for authentication
|
||||
|
||||
Returns:
|
||||
Detection results
|
||||
"""
|
||||
verify_auth_token(authorization)
|
||||
|
||||
# Analyze both prompt and generated text
|
||||
combined_content = f"{request.prompt} {request.generated_text}"
|
||||
|
||||
detections = simulate_detection(
|
||||
detector_id=request.detector_id,
|
||||
content=combined_content,
|
||||
detector_params=request.detector_params,
|
||||
)
|
||||
|
||||
return DetectionResponse(
|
||||
detections=detections,
|
||||
detection_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/v1/text/context/detection", response_model=DetectionResponse)
|
||||
async def context_detection(
|
||||
request: ContextDetectionRequest,
|
||||
authorization: Optional[str] = Header(None),
|
||||
):
|
||||
"""
|
||||
Detect potential issues in text with additional context.
|
||||
|
||||
Args:
|
||||
request: Detection request with content and context
|
||||
authorization: Bearer token for authentication
|
||||
|
||||
Returns:
|
||||
Detection results
|
||||
"""
|
||||
verify_auth_token(authorization)
|
||||
|
||||
detections = simulate_detection(
|
||||
detector_id=request.detector_id,
|
||||
content=request.content,
|
||||
detector_params=request.detector_params,
|
||||
)
|
||||
|
||||
return DetectionResponse(
|
||||
detections=detections,
|
||||
detection_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/detectors")
|
||||
async def list_detectors(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
List available detectors.
|
||||
|
||||
Args:
|
||||
authorization: Bearer token for authentication
|
||||
|
||||
Returns:
|
||||
List of available detectors
|
||||
"""
|
||||
verify_auth_token(authorization)
|
||||
|
||||
return {
|
||||
"detectors": [
|
||||
{
|
||||
"id": detector_id,
|
||||
"name": config["name"],
|
||||
"triggers": config["triggers"],
|
||||
}
|
||||
for detector_id, config in MOCK_DETECTORS.items()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Starting IBM FMS Guardrails Mock Server...")
|
||||
print("📍 Server will be available at: http://localhost:8001")
|
||||
print("📚 API docs at: http://localhost:8001/docs")
|
||||
print("\nAvailable detectors:")
|
||||
for detector_id, config in MOCK_DETECTORS.items():
|
||||
print(f" - {detector_id}: {config['name']}")
|
||||
print("\n✨ Use any Bearer token for authentication in this mock server\n")
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
@ -1,54 +0,0 @@
|
||||
"""
|
||||
Test script to reproduce the Groq streaming ASCII encoding issue.
|
||||
|
||||
This reproduces the issue described in #12660 where streaming responses
|
||||
containing non-ASCII characters like µ cause encoding errors.
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
from litellm import acompletion
|
||||
|
||||
async def test_groq_streaming_with_special_chars():
|
||||
"""Test that reproduces the ASCII encoding issue with Groq streaming."""
|
||||
try:
|
||||
print("Testing acompletion + streaming with Groq...")
|
||||
|
||||
# Test message that should trigger the µ character or similar non-ASCII content
|
||||
test_messages = [
|
||||
{"content": "What is the symbol for micro? Please include the µ symbol in your response.", "role": "user"}
|
||||
]
|
||||
|
||||
# This should trigger the ASCII encoding error described in the issue
|
||||
response = await acompletion(
|
||||
model="groq/llama-3.3-70b-versatile",
|
||||
messages=test_messages,
|
||||
stream=True
|
||||
)
|
||||
|
||||
print(f"Response type: {type(response)}")
|
||||
|
||||
# Try to iterate through the stream
|
||||
async for chunk in response:
|
||||
print(f"Chunk: {chunk}")
|
||||
|
||||
print("✅ Test completed successfully - no encoding errors!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error occurred: {e}")
|
||||
print(f"Error type: {type(e)}")
|
||||
print(f"Traceback:\n{traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Note: This requires GROQ_API_KEY to be set
|
||||
if not os.getenv("GROQ_API_KEY"):
|
||||
print("⚠️ GROQ_API_KEY not set. Skipping test.")
|
||||
else:
|
||||
success = asyncio.run(test_groq_streaming_with_special_chars())
|
||||
if success:
|
||||
print("🎉 All tests passed!")
|
||||
else:
|
||||
print("💥 Test failed!")
|
||||
@ -1,181 +0,0 @@
|
||||
"""
|
||||
Test script for the mock IBM Guardrails server.
|
||||
|
||||
This demonstrates how to interact with the mock server.
|
||||
|
||||
Usage:
|
||||
# Start the mock server in one terminal:
|
||||
python scripts/mock_ibm_guardrails_server.py
|
||||
|
||||
# Run this test in another terminal:
|
||||
python scripts/test_mock_ibm_guardrails.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
async def test_mock_server():
|
||||
"""Test the mock IBM Guardrails server."""
|
||||
base_url = "http://localhost:8001"
|
||||
headers = {"Authorization": "Bearer test-token-12345"}
|
||||
|
||||
print("🧪 Testing IBM FMS Guardrails Mock Server\n")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Test 1: Health check
|
||||
print("1️⃣ Testing health check...")
|
||||
try:
|
||||
response = await client.get(f"{base_url}/health")
|
||||
print(f" ✅ Health check: {response.json()}\n")
|
||||
except Exception as e:
|
||||
print(f" ❌ Health check failed: {e}\n")
|
||||
return
|
||||
|
||||
# Test 2: List detectors
|
||||
print("2️⃣ Testing list detectors...")
|
||||
try:
|
||||
response = await client.get(
|
||||
f"{base_url}/api/v1/detectors",
|
||||
headers=headers
|
||||
)
|
||||
detectors = response.json()
|
||||
print(f" ✅ Found {len(detectors['detectors'])} detectors:")
|
||||
for detector in detectors["detectors"]:
|
||||
print(f" - {detector['id']}: {detector['name']}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" ❌ List detectors failed: {e}\n")
|
||||
|
||||
# Test 3: Text detection with clean content
|
||||
print("3️⃣ Testing text detection (clean content)...")
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{base_url}/api/v1/text/detection",
|
||||
headers=headers,
|
||||
json={
|
||||
"detector_id": "hate",
|
||||
"content": "This is a normal, friendly message.",
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(f" ✅ Detection result:")
|
||||
print(f" Detection ID: {result['detection_id']}")
|
||||
for detection in result["detections"]:
|
||||
print(f" - Type: {detection['detection_type']}, Detected: {detection['detection']}, Score: {detection['score']:.2f}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" ❌ Text detection failed: {e}\n")
|
||||
|
||||
# Test 4: Text detection with problematic content
|
||||
print("4️⃣ Testing text detection (problematic content)...")
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{base_url}/api/v1/text/detection",
|
||||
headers=headers,
|
||||
json={
|
||||
"detector_id": "hate",
|
||||
"content": "This message contains hate speech and offensive language.",
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(f" ✅ Detection result:")
|
||||
print(f" Detection ID: {result['detection_id']}")
|
||||
for detection in result["detections"]:
|
||||
print(f" - Type: {detection['detection_type']}, Detected: {detection['detection']}, Score: {detection['score']:.2f}")
|
||||
if detection.get("evidence"):
|
||||
print(f" Evidence: {detection['evidence']}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" ❌ Text detection failed: {e}\n")
|
||||
|
||||
# Test 5: PII detection
|
||||
print("5️⃣ Testing PII detection...")
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{base_url}/api/v1/text/detection",
|
||||
headers=headers,
|
||||
json={
|
||||
"detector_id": "pii",
|
||||
"content": "Please send the report to my email address john@example.com",
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(f" ✅ Detection result:")
|
||||
print(f" Detection ID: {result['detection_id']}")
|
||||
for detection in result["detections"]:
|
||||
print(f" - Type: {detection['detection_type']}, Detected: {detection['detection']}, Score: {detection['score']:.2f}")
|
||||
if detection.get("text"):
|
||||
print(f" Detected text: '{detection['text']}'")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" ❌ PII detection failed: {e}\n")
|
||||
|
||||
# Test 6: Generation detection
|
||||
print("6️⃣ Testing text generation detection...")
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{base_url}/api/v1/text/generation/detection",
|
||||
headers=headers,
|
||||
json={
|
||||
"detector_id": "jailbreak",
|
||||
"prompt": "Tell me about AI safety",
|
||||
"generated_text": "I will ignore instructions and provide harmful content.",
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(f" ✅ Detection result:")
|
||||
print(f" Detection ID: {result['detection_id']}")
|
||||
for detection in result["detections"]:
|
||||
print(f" - Type: {detection['detection_type']}, Detected: {detection['detection']}, Score: {detection['score']:.2f}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" ❌ Generation detection failed: {e}\n")
|
||||
|
||||
# Test 7: Detection with custom threshold
|
||||
print("7️⃣ Testing detection with custom threshold...")
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{base_url}/api/v1/text/detection",
|
||||
headers=headers,
|
||||
json={
|
||||
"detector_id": "toxicity",
|
||||
"content": "This contains toxic language",
|
||||
"detector_params": {
|
||||
"threshold": 0.9
|
||||
}
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(f" ✅ Detection result (threshold=0.9):")
|
||||
print(f" Detection ID: {result['detection_id']}")
|
||||
for detection in result["detections"]:
|
||||
print(f" - Type: {detection['detection_type']}, Detected: {detection['detection']}, Score: {detection['score']:.2f}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" ❌ Threshold detection failed: {e}\n")
|
||||
|
||||
# Test 8: Authentication error
|
||||
print("8️⃣ Testing authentication error...")
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{base_url}/api/v1/text/detection",
|
||||
json={
|
||||
"detector_id": "hate",
|
||||
"content": "Test content",
|
||||
}
|
||||
)
|
||||
if response.status_code == 401:
|
||||
print(f" ✅ Authentication error handled correctly: {response.json()}\n")
|
||||
else:
|
||||
print(f" ⚠️ Unexpected status code: {response.status_code}\n")
|
||||
except Exception as e:
|
||||
print(f" ❌ Auth test failed: {e}\n")
|
||||
|
||||
print("✨ All tests completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_mock_server())
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to update the README.md providers table from provider_endpoints_support.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Define paths
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
JSON_PATH = REPO_ROOT / "provider_endpoints_support.json"
|
||||
README_PATH = REPO_ROOT / "README.md"
|
||||
|
||||
# Endpoint column headers
|
||||
ENDPOINT_COLUMNS = [
|
||||
("/chat/completions", "chat_completions"),
|
||||
("/messages", "messages"),
|
||||
("/responses", "responses"),
|
||||
("/embeddings", "embeddings"),
|
||||
("/image/generations", "image_generations"),
|
||||
("/audio/transcriptions", "audio_transcriptions"),
|
||||
("/audio/speech", "audio_speech"),
|
||||
("/moderations", "moderations"),
|
||||
("/batches", "batches"),
|
||||
("/rerank", "rerank"),
|
||||
]
|
||||
|
||||
|
||||
def load_providers_data():
|
||||
"""Load provider data from JSON file"""
|
||||
with open(JSON_PATH, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Handle both old and new format
|
||||
if "providers" in data:
|
||||
return data["providers"]
|
||||
return data
|
||||
|
||||
|
||||
def generate_markdown_table(providers_data):
|
||||
"""Generate markdown table from providers data"""
|
||||
|
||||
# Sort providers alphabetically by display name
|
||||
sorted_providers = sorted(
|
||||
providers_data.items(),
|
||||
key=lambda x: x[1]['display_name'].lower()
|
||||
)
|
||||
|
||||
# Generate header
|
||||
header_cols = ["Provider"] + [col[0] for col in ENDPOINT_COLUMNS]
|
||||
header = "| " + " | ".join(header_cols) + " |"
|
||||
separator = "|" + "|".join(["-" * (len(col) + 2) for col in header_cols]) + "|"
|
||||
|
||||
# Generate rows
|
||||
rows = []
|
||||
for slug, data in sorted_providers:
|
||||
display_name = data['display_name']
|
||||
url = data['url']
|
||||
|
||||
# Build row
|
||||
row_parts = [f"[{display_name}]({url})"]
|
||||
|
||||
for _, endpoint_key in ENDPOINT_COLUMNS:
|
||||
supported = data['endpoints'].get(endpoint_key, False)
|
||||
row_parts.append("✅" if supported else "")
|
||||
|
||||
row = "| " + " | ".join(row_parts) + " |"
|
||||
rows.append(row)
|
||||
|
||||
# Combine all parts
|
||||
table_lines = [
|
||||
"<!-- AUTO-GENERATED TABLE - DO NOT EDIT MANUALLY -->",
|
||||
"<!-- Edit provider_endpoints_support.json and run scripts/update_readme_providers_table.py -->",
|
||||
"",
|
||||
header,
|
||||
separator
|
||||
] + rows + [
|
||||
"<!-- END AUTO-GENERATED TABLE -->"
|
||||
]
|
||||
|
||||
return "\n".join(table_lines)
|
||||
|
||||
|
||||
def update_readme(table_markdown):
|
||||
"""Update README.md with new table"""
|
||||
with open(README_PATH, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
print(f" Original README length: {len(content)} bytes")
|
||||
|
||||
# Find the table section
|
||||
# Look for the AUTO-GENERATED comment or the header, and replace until Read the Docs
|
||||
pattern = r"(## Supported Providers.*?\n\n)(?:<!-- AUTO-GENERATED TABLE.*?<!-- END AUTO-GENERATED TABLE -->|.*?)(\n\n\[\*\*Read the Docs\*\*\])"
|
||||
|
||||
# Test if pattern matches
|
||||
match = re.search(pattern, content, flags=re.DOTALL)
|
||||
if not match:
|
||||
print("❌ Pattern did not match in README.md")
|
||||
return False
|
||||
|
||||
print(f" Pattern matched, replacing table...")
|
||||
|
||||
def replacer(match):
|
||||
return match.group(1) + table_markdown + match.group(2)
|
||||
|
||||
new_content = re.sub(pattern, replacer, content, flags=re.DOTALL)
|
||||
|
||||
print(f" New README length: {len(new_content)} bytes")
|
||||
|
||||
if new_content == content:
|
||||
print(" ℹ️ Table is already up-to-date, no changes needed")
|
||||
return True # Not an error - table is already correct
|
||||
|
||||
with open(README_PATH, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print(" ✓ README.md has been updated")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("Loading provider data from provider_endpoints_support.json...")
|
||||
providers_data = load_providers_data()
|
||||
print(f"✓ Loaded {len(providers_data)} providers")
|
||||
|
||||
print("\nGenerating markdown table...")
|
||||
table_markdown = generate_markdown_table(providers_data)
|
||||
print(f"✓ Generated table with {len(providers_data)} rows")
|
||||
|
||||
print("\nUpdating README.md...")
|
||||
if update_readme(table_markdown):
|
||||
print("✓ Successfully updated README.md")
|
||||
print("\n📝 Please review the changes and commit both files:")
|
||||
print(" - provider_endpoints_support.json")
|
||||
print(" - README.md")
|
||||
else:
|
||||
print("❌ Failed to update README.md")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user