Skip to content
6 changes: 6 additions & 0 deletions docs/serverless/utils/rp_upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ The upload utility provides functions to upload files and in-memory objects to a

*Note: The upload utility utilizes the Virtual-hosted-style URL with the bucket name in the host name. For example, `https: // bucket-name.s3.amazonaws.com`.*

## Requirements

The upload utility requires [boto3](https://pypi.org/project/boto3/) for S3 functionality. boto3 is lazy-loaded to minimize initial import time and memory footprint.

If you attempt to use S3 upload features without boto3 installed, you'll receive a warning and files will be saved to local disk instead (`simulated_uploaded/` or `local_upload/` directories).

## Bucket Credentials

You can set your S3 bucket credentials in the following ways:
Expand Down
48 changes: 40 additions & 8 deletions runpod/serverless/utils/rp_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@
import threading
import time
import uuid
from typing import Optional, Tuple
from typing import Any, Optional, Tuple
from urllib.parse import urlparse

import boto3
from boto3 import session
from boto3.s3.transfer import TransferConfig
from botocore.config import Config
from tqdm_loggable.auto import tqdm

logger = logging.getLogger("runpod upload utility")
Expand All @@ -43,12 +39,22 @@ def extract_region_from_url(endpoint_url):
# --------------------------- S3 Bucket Connection --------------------------- #
def get_boto_client(
bucket_creds: Optional[dict] = None,
) -> Tuple[
boto3.client, TransferConfig
]: # pragma: no cover # pylint: disable=line-too-long
) -> Tuple[Any, Any]: # pragma: no cover # pylint: disable=line-too-long
"""
Returns a boto3 client and transfer config for the bucket.
Lazy-loads boto3 to reduce initial import time.
"""
try:
from boto3 import session
from boto3.s3.transfer import TransferConfig
from botocore.config import Config
except ImportError:
logger.warning(
"boto3 not installed. S3 upload functionality disabled. "
"Install with: pip install boto3"
)
return None, None

bucket_session = session.Session()

boto_config = Config(
Expand Down Expand Up @@ -180,6 +186,18 @@ def bucket_upload(job_id, file_list, bucket_creds): # pragma: no cover
"""
Uploads files to bucket storage.
"""
try:
from boto3 import session
from botocore.config import Config
except ImportError:
logger.error(
"boto3 not installed. Cannot upload to S3 bucket. "
"Install with: pip install boto3"
)
raise ImportError(
"boto3 is required for bucket_upload. Install with: pip install boto3"
)

temp_bucket_session = session.Session()

temp_boto_config = Config(
Expand Down Expand Up @@ -285,6 +303,20 @@ def upload_in_memory_object(

key = f"{prefix}/{file_name}" if prefix else file_name

if boto_client is None:
print("No bucket endpoint set, saving to disk folder 'local_upload'")
print("If this is a live endpoint, please reference the following:")
print(
"https://github.com/runpod/runpod-python/blob/main/docs/serverless/utils/rp_upload.md"
)

os.makedirs("local_upload", exist_ok=True)
local_upload_location = f"local_upload/{file_name}"
with open(local_upload_location, "wb") as file_output:
file_output.write(file_data)

return local_upload_location

file_size = len(file_data)
with tqdm(
total=file_size, unit="B", unit_scale=True, desc=file_name
Expand Down
7 changes: 4 additions & 3 deletions tests/test_serverless/test_utils/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def test_get_boto_client(self):
# Define the bucket credentials
bucket_creds = BUCKET_CREDENTIALS

# Mock boto3.session.Session
# Mock boto3 imports (now lazy-loaded inside the function)
with patch("boto3.session.Session") as mock_session, patch(
"runpod.serverless.utils.rp_upload.TransferConfig"
"boto3.s3.transfer.TransferConfig"
) as mock_transfer_config:
mock_session.return_value.client.return_value = self.mock_boto_client
mock_transfer_config.return_value = self.mock_transfer_config
Expand Down Expand Up @@ -110,8 +110,9 @@ def test_get_boto_client_environ(self):

importlib.reload(rp_upload)

# Mock boto3 imports (now lazy-loaded inside the function)
with patch("boto3.session.Session") as mock_session, patch(
"runpod.serverless.utils.rp_upload.TransferConfig"
"boto3.s3.transfer.TransferConfig"
) as mock_transfer_config:
mock_session.return_value.client.return_value = self.mock_boto_client
mock_transfer_config.return_value = self.mock_transfer_config
Expand Down
Loading