Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
import os
|
|
import tempfile
|
|
import traceback
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
import magic
|
|
import requests
|
|
|
|
|
|
def is_url(path_or_url: str) -> bool:
|
|
"""
|
|
Check if the given string is a URL.
|
|
|
|
Args:
|
|
path_or_url: String to check
|
|
|
|
Returns:
|
|
bool: True if the string is a URL, False otherwise
|
|
"""
|
|
parsed = urlparse(path_or_url)
|
|
return bool(parsed.scheme and parsed.netloc)
|
|
|
|
|
|
def get_mime_type(file_path: str, default_mime: str | None = None) -> str:
|
|
"""
|
|
Detect MIME type of a file using python-magic if available,
|
|
otherwise fallback to extension-based detection.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
default_mime: Default MIME type to return if detection fails
|
|
|
|
Returns:
|
|
str: Detected MIME type
|
|
"""
|
|
# Try using python-magic for accurate MIME type detection
|
|
try:
|
|
mime = magic.Magic(mime=True)
|
|
return mime.from_file(file_path)
|
|
except (AttributeError, IOError):
|
|
# Fallback to extension-based detection
|
|
extension_mime_map = {
|
|
# Audio formats
|
|
".mp3": "audio/mpeg",
|
|
".wav": "audio/wav",
|
|
".ogg": "audio/ogg",
|
|
".m4a": "audio/mp4",
|
|
".flac": "audio/flac",
|
|
# Image formats
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".png": "image/png",
|
|
".gif": "image/gif",
|
|
".webp": "image/webp",
|
|
".bmp": "image/bmp",
|
|
".tiff": "image/tiff",
|
|
# Video formats
|
|
".mp4": "video/mp4",
|
|
".avi": "video/x-msvideo",
|
|
".mov": "video/quicktime",
|
|
".mkv": "video/x-matroska",
|
|
".webm": "video/webm",
|
|
}
|
|
|
|
ext = Path(file_path).suffix.lower()
|
|
return extension_mime_map.get(ext, default_mime or "application/octet-stream")
|
|
|
|
|
|
def get_file_from_source(
|
|
source: str,
|
|
max_size_mb: float = 100.0,
|
|
timeout: int = 60,
|
|
) -> tuple[str, str, bytes]:
|
|
"""
|
|
Unified function to get file content from a URL or local path with validation.
|
|
|
|
Args:
|
|
source: URL or local file path
|
|
max_size_mb: Maximum allowed file size in MB
|
|
timeout: Timeout for URL requests in seconds
|
|
|
|
Returns:
|
|
Tuple[str, str, bytes]: (file_path, mime_type, file_content)
|
|
- For URLs, file_path will be a temporary file path
|
|
- For local files, file_path will be the original path
|
|
|
|
Raises:
|
|
ValueError: When file doesn't exist, exceeds size limit, or has invalid MIME type
|
|
IOError: When file cannot be read
|
|
requests.RequestException: When URL request fails
|
|
"""
|
|
max_size_bytes = max_size_mb * 1024 * 1024
|
|
|
|
if is_url(source):
|
|
# Handle URL source
|
|
try:
|
|
# Make a HEAD request first to check content length
|
|
head_response = requests.head(source, timeout=timeout, allow_redirects=True)
|
|
head_response.raise_for_status()
|
|
|
|
# Check content length if available
|
|
content_length = head_response.headers.get("content-length")
|
|
if content_length and int(content_length) > max_size_bytes:
|
|
raise ValueError(
|
|
f"File size ({int(content_length) / (1024 * 1024):.2f} MB) "
|
|
f"exceeds maximum allowed size ({max_size_mb} MB)"
|
|
)
|
|
|
|
# Download the file
|
|
response = requests.get(source, timeout=timeout, stream=True)
|
|
response.raise_for_status()
|
|
|
|
# Read content with size checking
|
|
content = b""
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
if len(content) + len(chunk) > max_size_bytes:
|
|
raise ValueError(f"File size exceeds maximum allowed size ({max_size_mb} MB)")
|
|
content += chunk
|
|
|
|
# Create temporary file
|
|
parsed_url = urlparse(source)
|
|
filename = os.path.basename(parsed_url.path) or "downloaded_file"
|
|
|
|
# Create temporary file with proper extension
|
|
suffix = Path(filename).suffix or ".tmp"
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
|
temp_file.write(content)
|
|
temp_path = temp_file.name
|
|
|
|
# Get MIME type
|
|
mime_type = get_mime_type(temp_path)
|
|
|
|
return temp_path, mime_type, content
|
|
|
|
except requests.RequestException as e:
|
|
raise requests.RequestException(f"Failed to download file from URL: {e}: {traceback.format_exc()}")
|
|
except Exception as e:
|
|
raise IOError(f"Error processing URL: {e}: {traceback.format_exc()}") from e
|
|
|
|
else:
|
|
# Handle local file path
|
|
file_path = Path(source)
|
|
|
|
# Check if file exists
|
|
if not file_path.exists():
|
|
raise ValueError(f"File does not exist: {source}")
|
|
|
|
if not file_path.is_file():
|
|
raise ValueError(f"Path is not a file: {source}")
|
|
|
|
# Check file size
|
|
file_size = file_path.stat().st_size
|
|
if file_size > max_size_bytes:
|
|
raise ValueError(
|
|
f"File size ({file_size / (1024 * 1024):.2f} MB) exceeds maximum allowed size ({max_size_mb} MB)"
|
|
)
|
|
|
|
# Read file content
|
|
try:
|
|
with open(file_path, "rb") as f:
|
|
content = f.read()
|
|
except Exception as e:
|
|
raise IOError(f"Cannot read file {source}: {e}: {traceback.format_exc()}") from e
|
|
|
|
# Get MIME type
|
|
mime_type = get_mime_type(str(file_path))
|
|
|
|
return str(file_path), mime_type, content
|