Spaces:
Runtime error
Runtime error
Download utils/s3_client.py from Smartwaylabs/pipecat-livekit: direct link, hf CLI and curl.
- Browser
- Download file 5.32 kB
-
https://huggingface.co/spaces/Smartwaylabs/pipecat-livekit/resolve/main/utils/s3_client.py
- Command line
-
hf download hf://spaces/Smartwaylabs/pipecat-livekit/utils/s3_client.py
-
curl -L -o s3_client.py https://huggingface.co/spaces/Smartwaylabs/pipecat-livekit/resolve/main/utils/s3_client.py
5.32 kB
| """ | |
| Consolidated S3 client for all storage operations. | |
| Provides: | |
| - Environment-based key prefixing for staging/production isolation | |
| - Unified upload interface for transcripts, logs, and state | |
| - Validation of required environment variables | |
| """ | |
| import os | |
| from typing import Optional | |
| import boto3 | |
| from loguru import logger | |
| def get_s3_key_with_prefix(key: str) -> str: | |
| """ | |
| Add S3_PREFIX to key for data isolation between environments. | |
| If S3_PREFIX is empty or not set, returns key unchanged for backward | |
| compatibility with existing production data. | |
| Args: | |
| key: Original S3 key (e.g., 'transcripts/session.vtt') | |
| Returns: | |
| Prefixed key if S3_PREFIX is set (e.g., 'staging/transcripts/session.vtt') | |
| Original key if S3_PREFIX is empty (backward compatible for production) | |
| Examples: | |
| # Production (S3_PREFIX not set) | |
| >>> os.environ.pop('S3_PREFIX', None) | |
| >>> get_s3_key_with_prefix('transcripts/session.vtt') | |
| 'transcripts/session.vtt' | |
| # Staging (S3_PREFIX='staging') | |
| >>> os.environ['S3_PREFIX'] = 'staging' | |
| >>> get_s3_key_with_prefix('transcripts/session.vtt') | |
| 'staging/transcripts/session.vtt' | |
| """ | |
| prefix = os.environ.get("S3_PREFIX", "").strip() | |
| if not prefix: | |
| return key | |
| # Ensure prefix doesn't start/end with slashes | |
| prefix = prefix.strip("/") | |
| # Ensure key doesn't start with slash | |
| key = key.lstrip("/") | |
| return f"{prefix}/{key}" | |
| class S3Client: | |
| """ | |
| Unified S3 client for all storage operations. | |
| Centralizes S3 operations to avoid code duplication across modules. | |
| Validates environment variables and provides consistent error handling. | |
| """ | |
| REQUIRED_ENV_VARS = [ | |
| "AWS_ACCESS_KEY_ID", | |
| "AWS_SECRET_ACCESS_KEY", | |
| "BUCKET_REGION", | |
| "BUCKET_NAME", | |
| ] | |
| def __init__(self): | |
| self._client: Optional[boto3.client] = None | |
| self._bucket: Optional[str] = None | |
| def _ensure_initialized(self) -> bool: | |
| """Lazily initialize S3 client when first needed.""" | |
| if self._client is not None: | |
| return True | |
| missing_vars = [var for var in self.REQUIRED_ENV_VARS if not os.environ.get(var)] | |
| if missing_vars: | |
| logger.warning(f"S3Client: Missing environment variables: {missing_vars}") | |
| return False | |
| try: | |
| self._client = boto3.client( | |
| "s3", | |
| aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"], | |
| aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"], | |
| region_name=os.environ["BUCKET_REGION"], | |
| ) | |
| self._bucket = os.environ["BUCKET_NAME"] | |
| return True | |
| except Exception as e: | |
| logger.error(f"S3Client: Failed to initialize: {e}") | |
| return False | |
| def upload( | |
| self, | |
| key: str, | |
| body: str, | |
| content_type: str = "application/octet-stream", | |
| use_prefix: bool = True, | |
| ) -> bool: | |
| """ | |
| Upload content to S3. | |
| Args: | |
| key: S3 object key | |
| body: Content to upload | |
| content_type: MIME type of the content | |
| use_prefix: Whether to apply S3_PREFIX for environment isolation | |
| Returns: | |
| True if upload succeeded, False otherwise | |
| """ | |
| if not self._ensure_initialized(): | |
| return False | |
| try: | |
| final_key = get_s3_key_with_prefix(key) if use_prefix else key | |
| self._client.put_object( | |
| Bucket=self._bucket, | |
| Key=final_key, | |
| Body=body, | |
| ContentType=content_type, | |
| ) | |
| logger.info(f"S3Client: Uploaded to s3://{self._bucket}/{final_key}") | |
| return True | |
| except Exception as e: | |
| logger.error(f"S3Client: Upload failed for {key}: {e}") | |
| return False | |
| def upload_transcript(self, key: str, vtt_content: str) -> bool: | |
| """Upload VTT transcript to S3.""" | |
| return self.upload(key, vtt_content, content_type="text/vtt") | |
| def upload_json(self, key: str, json_content: str) -> bool: | |
| """Upload JSON content to S3.""" | |
| return self.upload(key, json_content, content_type="application/json") | |
| def upload_log(self, key: str, log_content: str) -> bool: | |
| """Upload log file to S3.""" | |
| if not key.endswith(".log"): | |
| key = f"{key}.log" | |
| return self.upload(key, log_content, content_type="text/plain") | |
| def upload_state(self, base_key: str, state_json: str) -> bool: | |
| """ | |
| Upload flow state to S3. | |
| Args: | |
| base_key: Base key (typically transcript key), will add _state.json suffix | |
| state_json: JSON string of flow state | |
| """ | |
| if base_key.endswith(".vtt"): | |
| state_key = base_key.replace(".vtt", "_state.json") | |
| else: | |
| state_key = f"{base_key}_state.json" | |
| return self.upload_json(state_key, state_json) | |
| # Global singleton for convenience | |
| _s3_client: Optional[S3Client] = None | |
| def get_s3_client() -> S3Client: | |
| """Get the global S3 client instance.""" | |
| global _s3_client | |
| if _s3_client is None: | |
| _s3_client = S3Client() | |
| return _s3_client | |