Metadata-Version: 2.4
Name: sirv-rest-api
Version: 1.0.0
Summary: Official Python SDK for Sirv REST API - Image CDN, 360 Spins, and Media Management
Author-email: Sirv <support@sirv.com>
Maintainer-email: Sirv <support@sirv.com>
License-Expression: MIT
Project-URL: Homepage, https://sirv.com
Project-URL: Documentation, https://apidocs.sirv.com/
Project-URL: Repository, https://github.com/sirv/sirv-rest-api-python
Project-URL: Issues, https://github.com/sirv/sirv-rest-api-python/issues
Project-URL: Changelog, https://github.com/sirv/sirv-rest-api-python/blob/main/CHANGELOG.md
Keywords: sirv,image,cdn,media,360,spin,zoom,api,rest,sdk,python,image-processing,image-optimization,media-management,digital-assets
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Requires-Dist: requests-toolbelt>=0.9.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: responses>=0.22.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: isort>=5.12.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: types-requests>=2.28.0; extra == "dev"
Provides-Extra: async
Requires-Dist: aiohttp>=3.8.0; extra == "async"
Requires-Dist: aiofiles>=23.0.0; extra == "async"
Dynamic: license-file

# Sirv REST API Python SDK

Official Python SDK for the [Sirv REST API](https://apidocs.sirv.com/) - Image CDN, 360 Spins, and Media Management.

[![PyPI version](https://badge.fury.io/py/sirv-rest-api.svg)](https://badge.fury.io/py/sirv-rest-api)
[![Python versions](https://img.shields.io/pypi/pyversions/sirv-rest-api.svg)](https://pypi.org/project/sirv-rest-api/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- **Full API Coverage**: All Sirv REST API endpoints implemented
- **Type Hints**: Complete type annotations for IDE support
- **Automatic Token Management**: Auto-refresh tokens before expiry
- **Pagination Helpers**: Generators for iterating through large result sets
- **Context Manager Support**: Use with `with` statement for automatic cleanup
- **Retry Logic**: Built-in retry mechanism for failed requests

## Installation

```bash
pip install sirv-rest-api
```

Or with optional async support:

```bash
pip install sirv-rest-api[async]
```

## Quick Start

```python
from sirv_rest_api import SirvClient

# Initialize the client
client = SirvClient(
    client_id="your-client-id",
    client_secret="your-client-secret"
)

# Connect to get access token
client.connect()

# Get account information
account = client.get_account_info()
print(f"CDN URL: {account['cdnURL']}")

# Upload a file
client.upload_file("/images/photo.jpg", "./local/photo.jpg")

# Search for files
results = client.search_files({"query": "*.jpg", "size": 100})
for file in results['hits']:
    print(file['filename'])
```

## Using Context Manager

```python
from sirv_rest_api import SirvClient

with SirvClient(
    client_id="your-client-id",
    client_secret="your-client-secret"
) as client:
    account = client.get_account_info()
    print(f"Account: {account['alias']}")
# Session automatically closed
```

## API Reference

### Authentication

#### Connect

Authenticate and obtain a bearer token. Tokens are automatically refreshed before expiry.

```python
token_info = client.connect()
print(f"Token expires in: {token_info['expiresIn']} seconds")
print(f"Scopes: {token_info['scope']}")

# Optional: Request a token with custom expiry time (5-604800 seconds)
token_info = client.connect(expires_in=3600)  # Token valid for 1 hour
```

The `connect()` method accepts an optional `expires_in` parameter to customize token lifetime:
- **Range**: 5 to 604800 seconds (7 days)
- **Default**: 1200 seconds (20 minutes)

#### Check Connection Status

```python
if client.is_connected():
    print("Client is connected")
```

### Account Management

#### Get Account Info

```python
account = client.get_account_info()
print(f"CDN URL: {account['cdnURL']}")
print(f"Alias: {account['alias']}")
print(f"Created: {account['dateCreated']}")
```

#### Update Account Settings

```python
client.update_account({
    "minify": {"enabled": True},
    "fetching": {
        "enabled": True,
        "type": "http",
        "http": {"url": "https://example.com/images"}
    }
})
```

#### Get API Rate Limits

```python
limits = client.get_account_limits()
print(f"REST API - Remaining: {limits['rest']['remaining']}/{limits['rest']['limit']}")
```

#### Get Storage Info

```python
storage = client.get_storage_info()
print(f"Used: {storage['used'] / 1e9:.2f} GB of {storage['allowance'] / 1e9:.2f} GB")
print(f"Files: {storage['files']}")
```

#### Get Account Users

```python
users = client.get_account_users()
for user in users:
    print(f"{user['email']} - {user['role']}")
```

#### Get Billing Plan

```python
plan = client.get_billing_plan()
print(f"Plan: {plan['name']}")
print(f"Storage: {plan['storage'] / 1e9:.2f} GB")
```

#### Search Account Events

```python
events = client.search_events({
    "level": "error",
    "module": "files",
    "from_": "2024-01-01",
    "to": "2024-01-31"
})
for event in events:
    print(f"{event['timestamp']}: {event['message']}")
```

#### Mark Events as Seen

```python
client.mark_events_seen(["event-id-1", "event-id-2"])
```

### User Management

#### Get User Info

```python
# Current user
user = client.get_user_info()
print(f"Email: {user['email']}")
print(f"S3 Key: {user.get('s3Key')}")

# Specific user
user = client.get_user_info(user_id="user-123")
```

### File Operations

#### Upload Files

```python
# Upload from file path
client.upload_file("/images/photo.jpg", "./local/photo.jpg")

# Upload from bytes
with open("photo.jpg", "rb") as f:
    photo_bytes = f.read()
client.upload_file("/images/photo.jpg", photo_bytes)

# Upload with options
client.upload_file(
    "/images/photo.jpg",
    "./local/photo.jpg",
    options={"content_type": "image/jpeg"}
)
```

#### Download Files

```python
# Download to bytes
content = client.download_file("/images/photo.jpg")

# Download to local file
client.download_file_to("/images/photo.jpg", "./downloads/photo.jpg")
```

#### Get File Info

```python
info = client.get_file_info("/images/photo.jpg")
print(f"Size: {info['size']} bytes")
print(f"Content-Type: {info['contentType']}")
print(f"Modified: {info['mtime']}")
```

#### Copy Files

```python
client.copy_file("/images/photo.jpg", "/backup/photo.jpg")
```

#### Rename/Move Files

```python
client.rename_file("/images/old-name.jpg", "/images/new-name.jpg")

# Move to different folder
client.rename_file("/images/photo.jpg", "/archive/photo.jpg")
```

#### Delete Files

```python
# Delete single file
client.delete_file("/images/old-photo.jpg")

# Batch delete
result = client.batch_delete(["/old/file1.jpg", "/old/file2.jpg", "/old/file3.jpg"])
print(f"Job ID: {result.get('jobId')}")

# Check batch delete status
status = client.get_batch_delete_status(result['jobId'])
print(f"Status: {status['status']}")
```

#### Fetch from URL

```python
client.fetch_url(
    url="https://example.com/image.jpg",
    filename="/fetched/image.jpg",
    wait=True  # Wait for completion
)
```

### Folder Operations

#### Create Folder

```python
client.create_folder("/images/2024")
```

#### Read Folder Contents

```python
# Single page
contents = client.read_folder_contents("/images")
for item in contents['contents']:
    if item['isDirectory']:
        print(f"[DIR] {item['filename']}")
    else:
        print(f"[FILE] {item['filename']} - {item['size']} bytes")

# With pagination
contents = client.read_folder_contents("/images")
while True:
    for item in contents['contents']:
        print(item['filename'])
    if not contents.get('continuation'):
        break
    contents = client.read_folder_contents("/images", contents['continuation'])
```

#### Iterate All Folder Contents

```python
# Automatically handles pagination
for item in client.iterate_folder_contents("/images"):
    print(f"{item['filename']}: {item.get('size', 'dir')}")
```

#### Folder Options

```python
# Get folder options
options = client.get_folder_options("/spins")
print(f"Scan spins: {options.get('scanSpins')}")

# Set folder options
client.set_folder_options("/spins", {"scanSpins": True})
```

### Search

#### Search Files

```python
results = client.search_files({
    "query": "*.jpg",
    "size": 100,
    "sort": {"field": "mtime", "order": "desc"},
    "filters": {
        "dirname": "/images",
        "minSize": 10000,
        "maxSize": 5000000
    }
})
print(f"Found {results['total']} files")
for file in results['hits']:
    print(f"{file['filename']} - {file['size']} bytes")
```

#### Paginated Search

```python
results = client.search_files({"query": "*.png"})
all_files = results['hits']

while results.get('scrollId') and results['hits']:
    results = client.scroll_search(results['scrollId'])
    all_files.extend(results['hits'])

print(f"Total files found: {len(all_files)}")
```

#### Iterate Search Results

```python
# Automatically handles pagination
for file in client.iterate_search_results({"query": "*.png", "size": 1000}):
    print(file['filename'])
```

### Metadata Management

#### File Metadata

```python
# Get all metadata
meta = client.get_file_meta("/images/photo.jpg")
print(f"Title: {meta.get('title')}")
print(f"Description: {meta.get('description')}")
print(f"Tags: {meta.get('tags')}")

# Set metadata
client.set_file_meta("/images/photo.jpg", {
    "title": "Beautiful Sunset",
    "description": "Photo taken at the beach during sunset",
    "tags": ["sunset", "beach", "nature", "landscape"]
})
```

#### Title

```python
# Get title
result = client.get_file_title("/images/photo.jpg")
print(result['title'])

# Set title
client.set_file_title("/images/photo.jpg", "My Beautiful Photo")
```

#### Description

```python
# Get description
result = client.get_file_description("/images/photo.jpg")
print(result['description'])

# Set description
client.set_file_description("/images/photo.jpg", "A stunning landscape photograph")
```

#### Tags

```python
# Get tags
result = client.get_file_tags("/images/photo.jpg")
print(result['tags'])

# Add tags
client.add_file_tags("/images/photo.jpg", ["nature", "landscape"])

# Remove tags
client.remove_file_tags("/images/photo.jpg", ["old-tag"])
```

#### Product Metadata

```python
# Get product metadata
product = client.get_product_meta("/products/item.jpg")
print(f"SKU: {product.get('sku')}")
print(f"Brand: {product.get('brand')}")

# Set product metadata
client.set_product_meta("/products/item.jpg", {
    "id": "PROD-123",
    "name": "Blue Widget",
    "sku": "BW-001",
    "brand": "Acme",
    "category": "Widgets"
})
```

#### Approval Flag

```python
# Get approval status
result = client.get_approval_flag("/images/photo.jpg")
print(f"Approved: {result['approved']}")

# Set approval flag
client.set_approval_flag("/images/photo.jpg", True)
```

### ZIP Operations

```python
# Create ZIP archive
result = client.batch_zip({
    "filenames": ["/images/1.jpg", "/images/2.jpg", "/images/3.jpg"],
    "filename": "/archives/photos.zip"
})
print(f"Job ID: {result.get('jobId')}")

# Check ZIP job status
status = client.get_zip_status(result['jobId'])
if status['status'] == 'completed':
    print(f"ZIP created: {status['filename']}")
```

### JWT Protected URLs

```python
jwt = client.generate_jwt({
    "filename": "/protected/image.jpg",
    "expiresIn": 3600,  # 1 hour
    "secureParams": {
        "w": "800",
        "h": "600"
    }
})
print(f"Protected URL: {jwt['url']}")
print(f"Token: {jwt['token']}")
```

### 360 Spin Operations

#### Convert Spin to Video

```python
result = client.spin2video({
    "filename": "/spins/product.spin",
    "options": {
        "width": 1920,
        "height": 1080,
        "loops": 2,
        "format": "mp4"
    }
})
print(f"Video created: {result['filename']}")
```

#### Convert Video to Spin

```python
result = client.video2spin({
    "filename": "/videos/turntable.mp4",
    "targetFilename": "/spins/product.spin",
    "options": {
        "frames": 36,
        "start": 0,
        "duration": 10
    }
})
print(f"Spin created: {result['filename']}")
```

#### Export Spin to Marketplaces

```python
# Amazon
client.export_spin_to_amazon({
    "filename": "/spins/product.spin",
    "asin": "B08N5WRWNW"
})

# Walmart
client.export_spin_to_walmart({
    "filename": "/spins/product.spin",
    "productId": "123456789"
})

# Home Depot
client.export_spin_to_home_depot({
    "filename": "/spins/product.spin",
    "productId": "123456789"
})

# Lowe's
client.export_spin_to_lowes({
    "filename": "/spins/product.spin",
    "productId": "123456789"
})

# Grainger
client.export_spin_to_grainger({
    "filename": "/spins/product.spin",
    "productId": "123456789"
})
```

### Points of Interest

```python
# Get POIs
pois = client.get_points_of_interest("/spins/product.spin")
for poi in pois:
    print(f"{poi['name']}: ({poi['x']}, {poi['y']}) frame {poi.get('frame')}")

# Set POI
client.set_point_of_interest("/spins/product.spin", {
    "name": "logo",
    "x": 0.5,
    "y": 0.3,
    "frame": 0
})

# Delete POI
client.delete_point_of_interest("/spins/product.spin", "logo")
```

### Statistics

#### HTTP Transfer Stats

```python
stats = client.get_http_stats("2024-01-01", "2024-01-31")
for day in stats:
    print(f"{day['date']}: {day['transfer'] / 1e9:.2f} GB transferred")
```

#### Spin Views Stats

```python
# Max 5-day range
stats = client.get_spin_views_stats("2024-01-01", "2024-01-05")
for day in stats:
    print(f"{day['date']}: {day['views']} views, {day.get('spins', 0)} interactions")
```

#### Storage Stats

```python
stats = client.get_storage_stats("2024-01-01", "2024-01-31")
for day in stats:
    print(f"{day['date']}: {day['storage'] / 1e9:.2f} GB, {day.get('files', 0)} files")
```

## Error Handling

```python
from sirv_rest_api import SirvClient, SirvApiError

client = SirvClient(
    client_id="your-client-id",
    client_secret="your-client-secret"
)

try:
    client.connect()
    info = client.get_file_info("/nonexistent/file.jpg")
except SirvApiError as e:
    print(f"API Error: {e.message}")
    print(f"Status Code: {e.status_code}")
    print(f"Error Code: {e.error_code}")
except Exception as e:
    print(f"Unexpected error: {e}")
```

## Configuration Options

```python
client = SirvClient(
    client_id="your-client-id",
    client_secret="your-client-secret",
    base_url="https://api.sirv.com",  # API base URL
    auto_refresh_token=True,           # Auto-refresh tokens (default: True)
    token_refresh_buffer=60,           # Seconds before expiry to refresh (default: 60)
    timeout=30,                        # Request timeout in seconds (default: 30)
    max_retries=3                      # Max retries for failed requests (default: 3)
)
```

## Getting Your API Credentials

1. Log in to your [Sirv account](https://my.sirv.com/)
2. Go to **Settings** > **API**
3. Create a new API client or use existing credentials
4. Copy your `Client ID` and `Client Secret`

## Links

- [Sirv Website](https://sirv.com)
- [API Documentation](https://apidocs.sirv.com/)
- [GitHub Repository](https://github.com/sirv/sirv-rest-api-python)
- [PyPI Package](https://pypi.org/project/sirv-rest-api/)

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Support

- Email: support@sirv.com
- Documentation: https://sirv.com/help/
- API Docs: https://apidocs.sirv.com/
