Source code for shapelib.core.cloud.providers.s3_provider
"""AWS S3 cloud storage provider."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Any
from shapelib.factories.cloud_registry import register_cloud_provider
if TYPE_CHECKING:
from shapelib.core.cloud.cloud_path import CloudPath
[docs]
@register_cloud_provider
class S3Provider:
"""
Cloud provider for AWS S3 storage.
Handles file operations using the boto3 library for s3:// protocol URLs.
Examples
--------
>>> provider = S3Provider()
>>> provider.supports(CloudPath.from_string("s3://bucket/file.fgb"))
True
Big-O
-----
All operations are O(n) where n is the file size.
"""
[docs]
def supports(self, cloud_path: CloudPath) -> bool:
"""
Check if this provider handles S3 protocol paths.
Parameters
----------
cloud_path : CloudPath
The parsed cloud path to check.
Returns
-------
bool
True if the path uses s3:// protocol.
"""
return cloud_path.is_s3_protocol
[docs]
def upload(self, source: Path, cloud_path: CloudPath) -> None:
"""
Upload a local file to S3.
Parameters
----------
source : Path
The local file to upload.
cloud_path : CloudPath
The S3 destination path (s3://bucket/key).
Raises
------
ImportError
If boto3 is not installed.
FileNotFoundError
If the source file does not exist.
ConnectionError
If the upload fails.
PermissionError
If access is denied.
"""
client = self._get_client()
try:
client.upload_file(str(source), cloud_path.s3_bucket, cloud_path.s3_key)
except Exception as error:
if "AccessDenied" in str(error) or "403" in str(error):
raise PermissionError(f"Access denied: {cloud_path.path}") from error
raise ConnectionError(f"S3 upload failed: {error}") from error
[docs]
def download(self, cloud_path: CloudPath, destination: Path) -> Path:
"""
Download a file from S3.
Parameters
----------
cloud_path : CloudPath
The S3 source path (s3://bucket/key).
destination : Path
The local destination path.
Returns
-------
Path
The path to the downloaded file.
Raises
------
ImportError
If boto3 is not installed.
FileNotFoundError
If the S3 object does not exist.
ConnectionError
If the download fails.
PermissionError
If access is denied.
"""
client = self._get_client()
try:
destination.parent.mkdir(parents=True, exist_ok=True)
client.download_file(cloud_path.s3_bucket, cloud_path.s3_key, str(destination))
return destination
except Exception as error:
error_str = str(error)
if "NoSuchKey" in error_str or "404" in error_str:
raise FileNotFoundError(f"Not found: {cloud_path.path}") from error
if "AccessDenied" in error_str or "403" in error_str:
raise PermissionError(f"Access denied: {cloud_path.path}") from error
raise ConnectionError(f"S3 download failed: {error}") from error
[docs]
def file_exists(self, cloud_path: CloudPath) -> bool:
"""
Check if an S3 object exists using head_object.
Parameters
----------
cloud_path : CloudPath
The S3 path to check.
Returns
-------
bool
True if the object exists, False otherwise.
"""
try:
from botocore.exceptions import ClientError
except ImportError:
return False
try:
client = self._get_client()
client.head_object(Bucket=cloud_path.s3_bucket, Key=cloud_path.s3_key)
return True
except ClientError:
return False
except Exception:
return False
def _get_client(self) -> Any:
"""
Get or create the S3 client.
Returns
-------
Any
The S3 client instance (boto3.client).
Raises
------
ImportError
If boto3 is not installed.
"""
try:
import boto3
except ImportError as error:
raise ImportError("boto3 is required for s3:// protocol. Install: pip install boto3") from error
return boto3.client("s3", region_name="sa-east-1")