style: Format all Python files with ruff
- Formatted 103 files to comply with ruff format requirements - No code logic changes, only formatting/whitespace - Fixes CI formatting check failures
This commit is contained in:
@@ -32,9 +32,9 @@ from .detector import ChangeDetector
|
||||
from .models import SyncConfig, ChangeReport, PageChange
|
||||
|
||||
__all__ = [
|
||||
'SyncMonitor',
|
||||
'ChangeDetector',
|
||||
'SyncConfig',
|
||||
'ChangeReport',
|
||||
'PageChange',
|
||||
"SyncMonitor",
|
||||
"ChangeDetector",
|
||||
"SyncConfig",
|
||||
"ChangeReport",
|
||||
"PageChange",
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@ class ChangeDetector:
|
||||
Returns:
|
||||
Hexadecimal hash string
|
||||
"""
|
||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||
|
||||
def fetch_page(self, url: str) -> tuple[str, dict[str, str]]:
|
||||
"""
|
||||
@@ -72,17 +72,15 @@ class ChangeDetector:
|
||||
requests.RequestException: If fetch fails
|
||||
"""
|
||||
response = requests.get(
|
||||
url,
|
||||
timeout=self.timeout,
|
||||
headers={'User-Agent': 'SkillSeekers-Sync/1.0'}
|
||||
url, timeout=self.timeout, headers={"User-Agent": "SkillSeekers-Sync/1.0"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
metadata = {
|
||||
'last-modified': response.headers.get('Last-Modified'),
|
||||
'etag': response.headers.get('ETag'),
|
||||
'content-type': response.headers.get('Content-Type'),
|
||||
'content-length': response.headers.get('Content-Length'),
|
||||
"last-modified": response.headers.get("Last-Modified"),
|
||||
"etag": response.headers.get("ETag"),
|
||||
"content-type": response.headers.get("Content-Type"),
|
||||
"content-length": response.headers.get("Content-Length"),
|
||||
}
|
||||
|
||||
return response.text, metadata
|
||||
@@ -92,7 +90,7 @@ class ChangeDetector:
|
||||
url: str,
|
||||
old_hash: str | None = None,
|
||||
generate_diff: bool = False,
|
||||
old_content: str | None = None
|
||||
old_content: str | None = None,
|
||||
) -> PageChange:
|
||||
"""
|
||||
Check if page has changed.
|
||||
@@ -132,7 +130,7 @@ class ChangeDetector:
|
||||
old_hash=old_hash,
|
||||
new_hash=new_hash,
|
||||
diff=diff,
|
||||
detected_at=datetime.utcnow()
|
||||
detected_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
except requests.RequestException:
|
||||
@@ -142,14 +140,11 @@ class ChangeDetector:
|
||||
change_type=ChangeType.DELETED,
|
||||
old_hash=old_hash,
|
||||
new_hash=None,
|
||||
detected_at=datetime.utcnow()
|
||||
detected_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
def check_pages(
|
||||
self,
|
||||
urls: list[str],
|
||||
previous_hashes: dict[str, str],
|
||||
generate_diffs: bool = False
|
||||
self, urls: list[str], previous_hashes: dict[str, str], generate_diffs: bool = False
|
||||
) -> ChangeReport:
|
||||
"""
|
||||
Check multiple pages for changes.
|
||||
@@ -185,13 +180,15 @@ class ChangeDetector:
|
||||
# Check for deleted pages (in previous state but not in current)
|
||||
for url, old_hash in previous_hashes.items():
|
||||
if url not in checked_urls:
|
||||
deleted.append(PageChange(
|
||||
url=url,
|
||||
change_type=ChangeType.DELETED,
|
||||
old_hash=old_hash,
|
||||
new_hash=None,
|
||||
detected_at=datetime.utcnow()
|
||||
))
|
||||
deleted.append(
|
||||
PageChange(
|
||||
url=url,
|
||||
change_type=ChangeType.DELETED,
|
||||
old_hash=old_hash,
|
||||
new_hash=None,
|
||||
detected_at=datetime.utcnow(),
|
||||
)
|
||||
)
|
||||
|
||||
return ChangeReport(
|
||||
skill_name="unknown", # To be set by caller
|
||||
@@ -200,7 +197,7 @@ class ChangeDetector:
|
||||
modified=modified,
|
||||
deleted=deleted,
|
||||
unchanged=unchanged_count,
|
||||
checked_at=datetime.utcnow()
|
||||
checked_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
def generate_diff(self, old_content: str, new_content: str) -> str:
|
||||
@@ -217,15 +214,9 @@ class ChangeDetector:
|
||||
old_lines = old_content.splitlines(keepends=True)
|
||||
new_lines = new_content.splitlines(keepends=True)
|
||||
|
||||
diff = difflib.unified_diff(
|
||||
old_lines,
|
||||
new_lines,
|
||||
fromfile='old',
|
||||
tofile='new',
|
||||
lineterm=''
|
||||
)
|
||||
diff = difflib.unified_diff(old_lines, new_lines, fromfile="old", tofile="new", lineterm="")
|
||||
|
||||
return ''.join(diff)
|
||||
return "".join(diff)
|
||||
|
||||
def generate_summary_diff(self, old_content: str, new_content: str) -> str:
|
||||
"""
|
||||
@@ -244,16 +235,15 @@ class ChangeDetector:
|
||||
diff = difflib.unified_diff(old_lines, new_lines)
|
||||
diff_lines = list(diff)
|
||||
|
||||
added = sum(1 for line in diff_lines if line.startswith('+') and not line.startswith('+++'))
|
||||
removed = sum(1 for line in diff_lines if line.startswith('-') and not line.startswith('---'))
|
||||
added = sum(1 for line in diff_lines if line.startswith("+") and not line.startswith("+++"))
|
||||
removed = sum(
|
||||
1 for line in diff_lines if line.startswith("-") and not line.startswith("---")
|
||||
)
|
||||
|
||||
return f"+{added} -{removed} lines"
|
||||
|
||||
def check_header_changes(
|
||||
self,
|
||||
url: str,
|
||||
old_modified: str | None = None,
|
||||
old_etag: str | None = None
|
||||
self, url: str, old_modified: str | None = None, old_etag: str | None = None
|
||||
) -> bool:
|
||||
"""
|
||||
Quick check using HTTP headers (no content download).
|
||||
@@ -269,14 +259,12 @@ class ChangeDetector:
|
||||
try:
|
||||
# Use HEAD request for efficiency
|
||||
response = requests.head(
|
||||
url,
|
||||
timeout=self.timeout,
|
||||
headers={'User-Agent': 'SkillSeekers-Sync/1.0'}
|
||||
url, timeout=self.timeout, headers={"User-Agent": "SkillSeekers-Sync/1.0"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
new_modified = response.headers.get('Last-Modified')
|
||||
new_etag = response.headers.get('ETag')
|
||||
new_modified = response.headers.get("Last-Modified")
|
||||
new_etag = response.headers.get("ETag")
|
||||
|
||||
# Check if headers indicate change
|
||||
if old_modified and new_modified and old_modified != new_modified:
|
||||
@@ -289,9 +277,7 @@ class ChangeDetector:
|
||||
return True
|
||||
|
||||
def batch_check_headers(
|
||||
self,
|
||||
urls: list[str],
|
||||
previous_metadata: dict[str, dict[str, str]]
|
||||
self, urls: list[str], previous_metadata: dict[str, dict[str, str]]
|
||||
) -> list[str]:
|
||||
"""
|
||||
Batch check URLs using headers only.
|
||||
@@ -307,8 +293,8 @@ class ChangeDetector:
|
||||
|
||||
for url in urls:
|
||||
old_meta = previous_metadata.get(url, {})
|
||||
old_modified = old_meta.get('last-modified')
|
||||
old_etag = old_meta.get('etag')
|
||||
old_modified = old_meta.get("last-modified")
|
||||
old_etag = old_meta.get("etag")
|
||||
|
||||
if self.check_header_changes(url, old_modified, old_etag):
|
||||
changed_urls.append(url)
|
||||
|
||||
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
class ChangeType(str, Enum):
|
||||
"""Type of change detected."""
|
||||
|
||||
ADDED = "added"
|
||||
MODIFIED = "modified"
|
||||
DELETED = "deleted"
|
||||
@@ -25,8 +26,7 @@ class PageChange(BaseModel):
|
||||
new_hash: str | None = Field(None, description="New content hash")
|
||||
diff: str | None = Field(None, description="Content diff (if available)")
|
||||
detected_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
description="When change was detected"
|
||||
default_factory=datetime.utcnow, description="When change was detected"
|
||||
)
|
||||
|
||||
class Config:
|
||||
@@ -37,7 +37,7 @@ class PageChange(BaseModel):
|
||||
"old_hash": "abc123",
|
||||
"new_hash": "def456",
|
||||
"diff": "@@ -10,3 +10,4 @@\n+New content here",
|
||||
"detected_at": "2024-01-15T10:30:00Z"
|
||||
"detected_at": "2024-01-15T10:30:00Z",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ class ChangeReport(BaseModel):
|
||||
deleted: list[PageChange] = Field(default_factory=list, description="Deleted pages")
|
||||
unchanged: int = Field(0, description="Number of unchanged pages")
|
||||
checked_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
description="When check was performed"
|
||||
default_factory=datetime.utcnow, description="When check was performed"
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -72,34 +71,19 @@ class SyncConfig(BaseModel):
|
||||
|
||||
skill_config: str = Field(..., description="Path to skill config file")
|
||||
check_interval: int = Field(
|
||||
default=3600,
|
||||
description="Check interval in seconds (default: 1 hour)"
|
||||
default=3600, description="Check interval in seconds (default: 1 hour)"
|
||||
)
|
||||
enabled: bool = Field(default=True, description="Whether sync is enabled")
|
||||
auto_update: bool = Field(
|
||||
default=False,
|
||||
description="Automatically rebuild skill on changes"
|
||||
)
|
||||
notify_on_change: bool = Field(
|
||||
default=True,
|
||||
description="Send notifications on changes"
|
||||
)
|
||||
auto_update: bool = Field(default=False, description="Automatically rebuild skill on changes")
|
||||
notify_on_change: bool = Field(default=True, description="Send notifications on changes")
|
||||
notification_channels: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Notification channels (email, slack, webhook)"
|
||||
)
|
||||
webhook_url: str | None = Field(
|
||||
None,
|
||||
description="Webhook URL for change notifications"
|
||||
default_factory=list, description="Notification channels (email, slack, webhook)"
|
||||
)
|
||||
webhook_url: str | None = Field(None, description="Webhook URL for change notifications")
|
||||
email_recipients: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Email recipients for notifications"
|
||||
)
|
||||
slack_webhook: str | None = Field(
|
||||
None,
|
||||
description="Slack webhook URL"
|
||||
default_factory=list, description="Email recipients for notifications"
|
||||
)
|
||||
slack_webhook: str | None = Field(None, description="Slack webhook URL")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
@@ -111,7 +95,7 @@ class SyncConfig(BaseModel):
|
||||
"notify_on_change": True,
|
||||
"notification_channels": ["slack", "webhook"],
|
||||
"webhook_url": "https://example.com/webhook",
|
||||
"slack_webhook": "https://hooks.slack.com/services/..."
|
||||
"slack_webhook": "https://hooks.slack.com/services/...",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +109,7 @@ class SyncState(BaseModel):
|
||||
total_checks: int = Field(default=0, description="Total checks performed")
|
||||
total_changes: int = Field(default=0, description="Total changes detected")
|
||||
page_hashes: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description="URL -> content hash mapping"
|
||||
default_factory=dict, description="URL -> content hash mapping"
|
||||
)
|
||||
status: str = Field(default="idle", description="Current status")
|
||||
error: str | None = Field(None, description="Last error message")
|
||||
@@ -137,15 +120,9 @@ class WebhookPayload(BaseModel):
|
||||
|
||||
event: str = Field(..., description="Event type (change_detected, sync_complete)")
|
||||
skill_name: str = Field(..., description="Skill name")
|
||||
timestamp: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
description="Event timestamp"
|
||||
)
|
||||
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Event timestamp")
|
||||
changes: ChangeReport | None = Field(None, description="Change report")
|
||||
metadata: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Additional metadata"
|
||||
)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
@@ -157,8 +134,8 @@ class WebhookPayload(BaseModel):
|
||||
"total_pages": 150,
|
||||
"added": [],
|
||||
"modified": [{"url": "https://react.dev/learn"}],
|
||||
"deleted": []
|
||||
"deleted": [],
|
||||
},
|
||||
"metadata": {"source": "periodic_check"}
|
||||
"metadata": {"source": "periodic_check"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class SyncMonitor:
|
||||
check_interval: int = 3600,
|
||||
auto_update: bool = False,
|
||||
state_file: str | None = None,
|
||||
on_change: Callable[[ChangeReport], None] | None = None
|
||||
on_change: Callable[[ChangeReport], None] | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize sync monitor.
|
||||
@@ -72,7 +72,7 @@ class SyncMonitor:
|
||||
with open(self.config_path) as f:
|
||||
self.skill_config = json.load(f)
|
||||
|
||||
self.skill_name = self.skill_config.get('name', 'unknown')
|
||||
self.skill_name = self.skill_config.get("name", "unknown")
|
||||
|
||||
# State file
|
||||
if state_file:
|
||||
@@ -97,10 +97,10 @@ class SyncMonitor:
|
||||
with open(self.state_file) as f:
|
||||
data = json.load(f)
|
||||
# Convert datetime strings back
|
||||
if data.get('last_check'):
|
||||
data['last_check'] = datetime.fromisoformat(data['last_check'])
|
||||
if data.get('last_change'):
|
||||
data['last_change'] = datetime.fromisoformat(data['last_change'])
|
||||
if data.get("last_check"):
|
||||
data["last_check"] = datetime.fromisoformat(data["last_check"])
|
||||
if data.get("last_change"):
|
||||
data["last_change"] = datetime.fromisoformat(data["last_change"])
|
||||
return SyncState(**data)
|
||||
else:
|
||||
return SyncState(skill_name=self.skill_name)
|
||||
@@ -109,12 +109,12 @@ class SyncMonitor:
|
||||
"""Save current state to file."""
|
||||
# Convert datetime to ISO format
|
||||
data = self.state.dict()
|
||||
if data.get('last_check'):
|
||||
data['last_check'] = data['last_check'].isoformat()
|
||||
if data.get('last_change'):
|
||||
data['last_change'] = data['last_change'].isoformat()
|
||||
if data.get("last_check"):
|
||||
data["last_check"] = data["last_check"].isoformat()
|
||||
if data.get("last_change"):
|
||||
data["last_change"] = data["last_change"].isoformat()
|
||||
|
||||
with open(self.state_file, 'w') as f:
|
||||
with open(self.state_file, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def check_now(self, generate_diffs: bool = False) -> ChangeReport:
|
||||
@@ -132,7 +132,7 @@ class SyncMonitor:
|
||||
|
||||
try:
|
||||
# Get URLs to check from config
|
||||
base_url = self.skill_config.get('base_url')
|
||||
base_url = self.skill_config.get("base_url")
|
||||
# TODO: In real implementation, get actual URLs from scraper
|
||||
|
||||
# For now, simulate with base URL only
|
||||
@@ -140,9 +140,7 @@ class SyncMonitor:
|
||||
|
||||
# Check for changes
|
||||
report = self.detector.check_pages(
|
||||
urls=urls,
|
||||
previous_hashes=self.state.page_hashes,
|
||||
generate_diffs=generate_diffs
|
||||
urls=urls, previous_hashes=self.state.page_hashes, generate_diffs=generate_diffs
|
||||
)
|
||||
report.skill_name = self.skill_name
|
||||
|
||||
@@ -192,7 +190,7 @@ class SyncMonitor:
|
||||
event="change_detected",
|
||||
skill_name=self.skill_name,
|
||||
changes=report,
|
||||
metadata={"auto_update": self.auto_update}
|
||||
metadata={"auto_update": self.auto_update},
|
||||
)
|
||||
|
||||
self.notifier.send(payload)
|
||||
@@ -214,9 +212,7 @@ class SyncMonitor:
|
||||
self._running = True
|
||||
|
||||
# Schedule checks
|
||||
schedule.every(self.check_interval).seconds.do(
|
||||
lambda: self.check_now()
|
||||
)
|
||||
schedule.every(self.check_interval).seconds.do(lambda: self.check_now())
|
||||
|
||||
# Run in thread
|
||||
def run_schedule():
|
||||
|
||||
@@ -34,7 +34,7 @@ class Notifier:
|
||||
webhook_url: str | None = None,
|
||||
slack_webhook: str | None = None,
|
||||
email_recipients: list[str] | None = None,
|
||||
console: bool = True
|
||||
console: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize notifier.
|
||||
@@ -45,8 +45,8 @@ class Notifier:
|
||||
email_recipients: List of email recipients
|
||||
console: Whether to print to console
|
||||
"""
|
||||
self.webhook_url = webhook_url or os.getenv('SYNC_WEBHOOK_URL')
|
||||
self.slack_webhook = slack_webhook or os.getenv('SLACK_WEBHOOK_URL')
|
||||
self.webhook_url = webhook_url or os.getenv("SYNC_WEBHOOK_URL")
|
||||
self.slack_webhook = slack_webhook or os.getenv("SLACK_WEBHOOK_URL")
|
||||
self.email_recipients = email_recipients or []
|
||||
self.console = console
|
||||
|
||||
@@ -92,8 +92,8 @@ class Notifier:
|
||||
response = requests.post(
|
||||
self.webhook_url,
|
||||
json=payload.dict(),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
timeout=10
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"✅ Webhook notification sent to {self.webhook_url}")
|
||||
@@ -124,14 +124,10 @@ class Notifier:
|
||||
slack_payload = {
|
||||
"text": text,
|
||||
"username": "Skill Seekers Sync",
|
||||
"icon_emoji": ":books:"
|
||||
"icon_emoji": ":books:",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
self.slack_webhook,
|
||||
json=slack_payload,
|
||||
timeout=10
|
||||
)
|
||||
response = requests.post(self.slack_webhook, json=slack_payload, timeout=10)
|
||||
response.raise_for_status()
|
||||
print("✅ Slack notification sent")
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user