style: Apply ruff auto-fixes for Python 3.12

- Update type annotations to modern syntax (dict, list, X | Y)
- Remove unnecessary elif after return
- Minor style improvements

Note: Some linting warnings remain (unused content param, inline conditions)
but these are minor style issues and do not affect functionality.
All tests pass with these changes.
This commit is contained in:
Bendt
2025-12-28 10:55:31 -05:00
parent 78ab945a4d
commit b1cd99abf2
3 changed files with 94 additions and 33 deletions

View File

@@ -523,7 +523,70 @@ class IPCClient:
## Mail Rendering Improvements ## Mail Rendering Improvements
Is there a way to improve readability of emails in a terminal? I get a lot of "notification style emails", and they aren't easy to comprehend or read in plain text. At the very least they don't look good. Maybe we can find an algorithm to reduce the visual noise. Or maybe an AI summary view option using copilot APIs? Research best options. Perhaps embedding a small pre-trained model that can do the summary? ### Smart Notification Email Compression (COMPLETED)
**Priority:** High
**Files:** `src/mail/notification_detector.py`, `src/mail/notification_compressor.py`, `src/mail/config.py`
**Problem:** Transactional notification emails from tools like Renovate, Jira, Confluence, and Datadog are hard to parse and display poorly in plain text terminal environments.
**Solution:** Implemented intelligent notification detection and compression system:
1. **Notification Type Detection**
- Automatically detects emails from:
- GitLab (pipelines, MRs, mentions)
- GitHub (PRs, issues, reviews)
- Jira (issues, status changes)
- Confluence (page updates, comments)
- Datadog (alerts, incidents)
- Renovate (dependency updates)
- General notifications (digests, automated emails)
- Uses sender domain and subject pattern matching
- Distinguishes between similar services (e.g., Jira vs Confluence)
2. **Structured Summary Extraction**
- Type-specific extractors for each platform
- Extracts: IDs, titles, status changes, action items
- Preserves important links for quick access
3. **Terminal-Friendly Formatting**
- Two compression modes:
- `summary`: Brief one-page view
- `detailed`: Structured table format
- Markdown rendering with icons and clear hierarchy
- Shows notification type, key details, actions
- Footer indicates compressed view (toggle to full with `m`)
4. **Configuration Options**
```toml
[content_display]
compress_notifications = true
notification_compression_mode = "summary" # "summary", "detailed", or "off"
```
5. **Features**
- Zero-dependency (no LLM required, fast)
- Rule-based for reliability
- Extensible to add new notification types
- Preserves original content for full view toggle
- Type-specific icons using NerdFont glyphs
**Implementation Details:**
- `NotificationType` dataclass for type definitions
- `is_notification_email()` for detection
- `classify_notification()` for type classification
- `extract_notification_summary()` for structured data
- `NotificationCompressor` for formatting
- `DetailedCompressor` for extended summaries
- Integrated with `ContentContainer` widget
- 13 unit tests covering all notification types
**Future Enhancements:**
- Add LLM-based summarization option (opt-in)
- Learn notification patterns from user feedback
- Custom notification type definitions
- Action-based email filtering (e.g., "archive all Renovate emails")
---
--- ---

View File

@@ -1,13 +1,12 @@
"""Notification email compressor for terminal-friendly display.""" """Notification email compressor for terminal-friendly display."""
from typing import Dict, Any, Optional from typing import Any
from textual.widgets import Markdown
from .notification_detector import ( from .notification_detector import (
is_notification_email, NotificationType,
classify_notification, classify_notification,
extract_notification_summary, extract_notification_summary,
NotificationType, is_notification_email,
) )
@@ -22,7 +21,7 @@ class NotificationCompressor:
""" """
self.mode = mode self.mode = mode
def should_compress(self, envelope: Dict[str, Any]) -> bool: def should_compress(self, envelope: dict[str, Any]) -> bool:
"""Check if email should be compressed. """Check if email should be compressed.
Args: Args:
@@ -37,8 +36,8 @@ class NotificationCompressor:
return is_notification_email(envelope) return is_notification_email(envelope)
def compress( def compress(
self, content: str, envelope: Dict[str, Any] self, content: str, envelope: dict[str, Any]
) -> tuple[str, Optional[NotificationType]]: ) -> tuple[str, NotificationType | None]:
"""Compress notification email content. """Compress notification email content.
Args: Args:
@@ -65,9 +64,9 @@ class NotificationCompressor:
def _format_as_markdown( def _format_as_markdown(
self, self,
summary: Dict[str, Any], summary: dict[str, Any],
envelope: Dict[str, Any], envelope: dict[str, Any],
notif_type: Optional[NotificationType], notif_type: NotificationType | None,
) -> str: ) -> str:
"""Format summary as markdown for terminal display. """Format summary as markdown for terminal display.
@@ -138,9 +137,9 @@ class DetailedCompressor(NotificationCompressor):
def _format_as_markdown( def _format_as_markdown(
self, self,
summary: Dict[str, Any], summary: dict[str, Any],
envelope: Dict[str, Any], envelope: dict[str, Any],
notif_type: Optional[NotificationType], notif_type: NotificationType | None,
) -> str: ) -> str:
"""Format summary with more detail.""" """Format summary with more detail."""
@@ -217,5 +216,4 @@ def create_compressor(mode: str) -> NotificationCompressor:
if mode == "detailed": if mode == "detailed":
return DetailedCompressor(mode=mode) return DetailedCompressor(mode=mode)
else: return NotificationCompressor(mode=mode)
return NotificationCompressor(mode=mode)

View File

@@ -1,8 +1,8 @@
"""Email notification detection utilities.""" """Email notification detection utilities."""
import re import re
from typing import Dict, Any, List, Optional
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
@dataclass @dataclass
@@ -10,11 +10,11 @@ class NotificationType:
"""Classification of notification email types.""" """Classification of notification email types."""
name: str name: str
patterns: List[str] patterns: list[str]
domains: List[str] domains: list[str]
icon: str icon: str
def matches(self, envelope: Dict[str, Any], content: Optional[str] = None) -> bool: def matches(self, envelope: dict[str, Any], content: str | None = None) -> bool:
"""Check if envelope matches this notification type.""" """Check if envelope matches this notification type."""
# Check sender domain (more specific check) # Check sender domain (more specific check)
@@ -24,7 +24,7 @@ class NotificationType:
if domain == "atlassian.net": if domain == "atlassian.net":
if "jira@" in from_addr: if "jira@" in from_addr:
return self.name == "jira" return self.name == "jira"
elif "confluence@" in from_addr: if "confluence@" in from_addr:
return self.name == "confluence" return self.name == "confluence"
elif domain in from_addr: elif domain in from_addr:
return True return True
@@ -85,7 +85,7 @@ NOTIFICATION_TYPES = [
def is_notification_email( def is_notification_email(
envelope: Dict[str, Any], content: Optional[str] = None envelope: dict[str, Any], content: str | None = None
) -> bool: ) -> bool:
"""Check if an email is a notification-style email. """Check if an email is a notification-style email.
@@ -127,8 +127,8 @@ def is_notification_email(
def classify_notification( def classify_notification(
envelope: Dict[str, Any], content: Optional[str] = None envelope: dict[str, Any], content: str | None = None
) -> Optional[NotificationType]: ) -> NotificationType | None:
"""Classify the type of notification email. """Classify the type of notification email.
Args: Args:
@@ -147,8 +147,8 @@ def classify_notification(
def extract_notification_summary( def extract_notification_summary(
content: str, notification_type: Optional[NotificationType] = None content: str, notification_type: NotificationType | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Extract structured summary from notification email content. """Extract structured summary from notification email content.
Args: Args:
@@ -185,7 +185,7 @@ def extract_notification_summary(
return summary return summary
def _extract_gitlab_summary(content: str) -> Dict[str, Any]: def _extract_gitlab_summary(content: str) -> dict[str, Any]:
"""Extract summary from GitLab notification.""" """Extract summary from GitLab notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}
@@ -219,7 +219,7 @@ def _extract_gitlab_summary(content: str) -> Dict[str, Any]:
return summary return summary
def _extract_github_summary(content: str) -> Dict[str, Any]: def _extract_github_summary(content: str) -> dict[str, Any]:
"""Extract summary from GitHub notification.""" """Extract summary from GitHub notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}
@@ -237,7 +237,7 @@ def _extract_github_summary(content: str) -> Dict[str, Any]:
return summary return summary
def _extract_jira_summary(content: str) -> Dict[str, Any]: def _extract_jira_summary(content: str) -> dict[str, Any]:
"""Extract summary from Jira notification.""" """Extract summary from Jira notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}
@@ -263,7 +263,7 @@ def _extract_jira_summary(content: str) -> Dict[str, Any]:
return summary return summary
def _extract_confluence_summary(content: str) -> Dict[str, Any]: def _extract_confluence_summary(content: str) -> dict[str, Any]:
"""Extract summary from Confluence notification.""" """Extract summary from Confluence notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}
@@ -283,7 +283,7 @@ def _extract_confluence_summary(content: str) -> Dict[str, Any]:
return summary return summary
def _extract_datadog_summary(content: str) -> Dict[str, Any]: def _extract_datadog_summary(content: str) -> dict[str, Any]:
"""Extract summary from Datadog notification.""" """Extract summary from Datadog notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}
@@ -303,7 +303,7 @@ def _extract_datadog_summary(content: str) -> Dict[str, Any]:
return summary return summary
def _extract_renovate_summary(content: str) -> Dict[str, Any]: def _extract_renovate_summary(content: str) -> dict[str, Any]:
"""Extract summary from Renovate notification.""" """Extract summary from Renovate notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}
@@ -320,7 +320,7 @@ def _extract_renovate_summary(content: str) -> Dict[str, Any]:
return summary return summary
def _extract_general_notification_summary(content: str) -> Dict[str, Any]: def _extract_general_notification_summary(content: str) -> dict[str, Any]:
"""Extract summary from general notification.""" """Extract summary from general notification."""
summary = {"action_items": [], "key_links": [], "metadata": {}} summary = {"action_items": [], "key_links": [], "metadata": {}}