Metadata-Version: 2.4
Name: universal-ads-sdk
Version: 2.1.1
Summary: Python SDK for the Universal Ads Third Party API
Home-page: https://github.com/Universal-Ads/universal-ads-sdk-python
Author: Universal Ads
Author-email: Universal Ads <support+sdk@universalads.com>
License: MIT
Project-URL: Homepage, https://github.com/universal-ads/universal-ads-sdk-python
Project-URL: Documentation, https://developers.universalads.com/
Project-URL: Repository, https://github.com/universal-ads/universal-ads-sdk-python
Project-URL: Bug Tracker, https://github.com/universal-ads/universal-ads-sdk-python/issues
Keywords: ads,advertising,api,sdk,universal-ads
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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 :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Requires-Dist: cryptography>=3.4.0
Requires-Dist: urllib3>=1.26.0
Provides-Extra: dev
Requires-Dist: pytest>=6.0; extra == "dev"
Requires-Dist: pytest-cov>=2.0; extra == "dev"
Requires-Dist: black>=21.0; extra == "dev"
Requires-Dist: flake8>=3.8; extra == "dev"
Requires-Dist: mypy>=0.800; extra == "dev"
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# Universal Ads SDK

A Python SDK for interacting with the Universal Ads Third Party API. This SDK provides a simple and intuitive interface for managing campaigns, ad sets, ads, creatives, pixels, media uploads, custom audiences, and performance reports.

## Features

- **Creative Management**: Create, read, update, and delete creatives
- **Campaign, Ad Set, and Ad Management**: List, create, get, and update campaign resources
- **Pixel Access**: List pixels, get pixel details, and retrieve pixel events
- **Media Upload**: Upload and verify media files
- **Audience Management**: Create and manage custom audiences for targeted advertising
- **Reports**: Access campaign, adset, and ad performance data
- **Automatic Retries**: Built-in retry logic for robust API interactions
- **Type Hints**: Full type annotation support for better development experience

## Installation

```bash
pip install universal-ads-sdk
```

## Quick Start

### 1. Initialize the Client

```python
from universal_ads_sdk import UniversalAdsClient

# Initialize the client with your API credentials
client = UniversalAdsClient(
    api_key="your-api-key",
    private_key_pem="""-----BEGIN PRIVATE KEY-----
your-private-key-content
-----END PRIVATE KEY-----"""
)
```

### 2. Upload Media

```python
# Upload a media file (using new API format)
upload_info = client.upload_media(
    mime_type="image/jpeg",
    adaccount_id="3d49e08c-465d-4673-a445-d4ba3575f032",
    name="my-image.jpg"
)

# Use the presigned URL to upload your file
import requests
with open("/path/to/your/image.jpg", "rb") as f:
    requests.put(upload_info["upload_url"], data=f)

# Verify the upload
media = client.verify_media(upload_info["id"])
print(f"Media verified: {media['status']}")

# Get media information
media_info = client.get_media(upload_info["id"])
print(f"Media filename: {media_info['filename']}")
```

### 3. Create a Creative

```python
# Create a new creative
creative = client.create_creative(
    adaccount_id="3d49e08c-465d-4673-a445-d4ba3575f032",
    name="My Creative",
    media_id="cc0f46c7-d9b9-4758-9479-17e1d77c5eea"
)
print(f"Created creative: {creative['id']}")
```

### 4. Create a Custom Audience

```python
# Option 1: Create audience with uploaded media file
# First, upload a CSV or TXT file containing user identifiers (one per row)
upload_info = client.upload_media(
    mime_type="text/csv",
    adaccount_id="3d49e08c-465d-4673-a445-d4ba3575f032",
    name="users.csv"
)

# Upload the file to the presigned URL
import requests
with open("/path/to/users.csv", "rb") as f:
    requests.put(upload_info["upload_url"], data=f)

# Verify the upload
media = client.verify_media(upload_info["id"])

# Create a new custom audience using the uploaded media
audience = client.create_audience(
    adaccount_id="3d49e08c-465d-4673-a445-d4ba3575f032",
    name="My Custom Audience",
    segment_type="email",
    media_id=upload_info["id"],
    description="An audience for targeted advertising"
)
print(f"Created audience: {audience['id']}")

# Option 2: Create audience with users list directly (max 10,000 users)
audience = client.create_audience(
    adaccount_id="3d49e08c-465d-4673-a445-d4ba3575f032",
    name="My Custom Audience",
    segment_type="email",
    users=["user@example.com", "another@example.com"],
    description="An audience created with user list"
)

# Add or remove users from an existing audience
client.update_audience_users(
    audience_id=audience["id"],
    users=["user@example.com", "another@example.com"],
    remove=False
)
```

### 5. Get Performance Reports

```python
# Get campaign performance report
report = client.get_campaign_report(
    adaccount_id="3d49e08c-465d-4673-a445-d4ba3575f032",
    start_date="2024-01-01T00:00:00",
    end_date="2024-01-31T23:59:59",
    date_aggregation="DAY",
    attribution_window="7_day"
)
print(f"Report contains {len(report['data'])} campaigns")

# Schedule an advanced report for asynchronous processing
scheduled_report = client.schedule_report(
    start_date="2024-01-01T00:00:00",
    end_date="2024-01-31T23:59:59",
    entity_level="campaign",
    adaccount_ids=["3d49e08c-465d-4673-a445-d4ba3575f032"],
    dimensions=["state", "dma"],
    time_aggregation="day"
)
print(f"Scheduled report ID: {scheduled_report['scheduled_report_id']}")

# Check scheduled report status
report_status = client.get_scheduled_report(scheduled_report['scheduled_report_id'])
print(f"Report status: {report_status['status']}")
```

### 6. Get Organizations and Ad Accounts

```python
# Get all organizations
organizations = client.get_organizations(limit=50)
print(f"Found {len(organizations['data'])} organizations")

# Get all ad accounts
adaccounts = client.get_adaccounts(limit=50)
print(f"Found {len(adaccounts['data'])} ad accounts")
```

## API Reference

### Client Initialization

```python
UniversalAdsClient(
    api_key: str,                    # Your API key
    private_key_pem: str,            # Your private key in PEM format
    base_url: Optional[str] = None,  # API base URL (defaults to production)
    timeout: int = 30,               # Request timeout in seconds
    max_retries: int = 3,            # Maximum retry attempts
    headers: Optional[Dict[str, str]] = None  # Optional headers added to every request
)
```

### Creative Management

#### Get All Creatives
```python
creatives = client.get_creatives(
    adaccount_id="account-id",  # Optional: filter by account
    campaign_id="campaign-id",  # Optional: filter by campaign
    adset_id="adset-id",        # Optional: filter by adset
    ad_id="ad-id",              # Optional: filter by ad
    limit=50,                    # Optional: limit results
    offset=0,                    # Optional: pagination offset
    sort="name_asc"              # Optional: sort order
)
```

#### Get Specific Creative
```python
creative = client.get_creative("creative-id")
```

#### Create Creative
```python
creative = client.create_creative(
    adaccount_id="account-id",
    name="Creative Name",
    media_id="media-id"
)
```

#### Update Creative
```python
creative = client.update_creative(
    "creative-id",
    name="New Name"
)
```

#### Delete Creative
```python
client.delete_creative("creative-id")
```

### Media Management

#### Upload Media
```python
# New API format
upload_info = client.upload_media(
    mime_type="image/jpeg",
    adaccount_id="account-id",
    name="my-image.jpg"
)


```

#### Get Media
```python
media = client.get_media("media-id")
```

#### Verify Media
```python
media = client.verify_media("media-id")
```

### Reporting

#### Campaign Report
```python
report = client.get_campaign_report(
    adaccount_id="account-id",      # Required
    start_date="2024-01-01T00:00:00",  # Optional (YYYY-MM-DDTHH:MM:SS)
    end_date="2024-01-31T23:59:59",    # Optional (YYYY-MM-DDTHH:MM:SS)
    campaign_ids=["id1", "id2"],   # Optional
    adset_ids=["id1", "id2"],       # Optional
    ad_ids=["id1", "id2"],          # Optional
    date_aggregation="DAY",         # Optional: HOUR, DAY, LIFETIME, TOTAL
    attribution_window="7_day",     # Optional: 7_day, 14_day, 30_day
    limit=100,                      # Optional
    offset=0                        # Optional
)
```

#### Adset Report
```python
report = client.get_adset_report(
    adaccount_id="account-id",      # Required
    start_date="2024-01-01T00:00:00",  # Optional (YYYY-MM-DDTHH:MM:SS)
    end_date="2024-01-31T23:59:59",    # Optional (YYYY-MM-DDTHH:MM:SS)
    campaign_ids=["id1", "id2"],   # Optional
    adset_ids=["id1", "id2"],       # Optional
    ad_ids=["id1", "id2"],          # Optional
    date_aggregation="DAY",          # Optional: HOUR, DAY, LIFETIME, TOTAL
    attribution_window="7_day",      # Optional: 7_day, 14_day, 30_day
    limit=100,                       # Optional
    offset=0                         # Optional
)
```

#### Ad Report
```python
report = client.get_ad_report(
    adaccount_id="account-id",      # Required
    start_date="2024-01-01T00:00:00",  # Optional (YYYY-MM-DDTHH:MM:SS)
    end_date="2024-01-31T23:59:59",    # Optional (YYYY-MM-DDTHH:MM:SS)
    campaign_ids=["id1", "id2"],   # Optional
    adset_ids=["id1", "id2"],       # Optional
    ad_ids=["id1", "id2"],          # Optional
    date_aggregation="DAY",          # Optional: HOUR, DAY, LIFETIME, TOTAL
    attribution_window="7_day",     # Optional: 7_day, 14_day, 30_day
    limit=100,                       # Optional
    offset=0                         # Optional
)
```

#### Schedule Report
```python
scheduled_report = client.schedule_report(
    start_date="2024-01-01T00:00:00",
    end_date="2024-01-31T23:59:59",
    entity_level="campaign",         # campaign, adset, or ad
    adaccount_ids=["account-id"],
    campaign_ids=["id1", "id2"],    # Optional
    adset_ids=["id1", "id2"],       # Optional
    ad_ids=["id1", "id2"],          # Optional
    dimensions=["state", "dma"],     # Optional: device_type, dma, state, zip_code
    time_aggregation="day",          # Optional: hour, day, or total
    attribution_window="7_day",      # Optional: 7_day, 14_day, 30_day
    limit=50000                      # Optional, default: 100000
)
```

#### Get Scheduled Report
```python
report_status = client.get_scheduled_report("scheduled-report-id")
```

### Campaign Management

#### Get All Campaigns
```python
campaigns = client.get_campaigns(
    adaccount_id="account-id",             # Required
    campaign_ids=["campaign-1"],           # Optional: filter by campaign IDs
    name="Spring Campaign",                # Optional
    status="active",                       # Optional
    campaign_type="performance",           # Optional: performance, content_select
    include_archived=False,                # Optional
    limit=50,                              # Optional
    offset=0,                              # Optional
    sort="id_asc"                          # Optional
)
```

#### Create / Update Campaign
```python
campaign = client.create_campaign(
    adaccount_id="account-id",
    name="Spring Campaign",
    objective="web_conversions"
)

updated = client.update_campaign(campaign["id"], name="Spring Campaign v2")
```

### Ad Set Management

#### Get All Ad Sets
```python
adsets = client.get_adsets(
    adaccount_id="account-id",             # Required
    campaign_ids=["campaign-id"],          # Optional
    adset_ids=["adset-id"],                # Optional
    name="Adults 25-44",                   # Optional
    status=["active", "paused"],           # Optional
    include_archived=False,                # Optional
    limit=50,                              # Optional
    offset=0,                              # Optional
    sort="id_asc"                          # Optional
)
```

#### Create / Update Ad Set
```python
adset = client.create_adset(
    adaccount_id="account-id",
    campaign_id="campaign-id",
    name="Adults 25-44"
)

updated = client.update_adset(adset["id"], name="Adults 25-54")
```

### Ad Management

#### Get All Ads
```python
ads = client.get_ads(
    adaccount_id="account-id",             # Required
    campaign_ids=["campaign-id"],          # Optional
    adset_ids=["adset-id"],                # Optional
    ad_ids=["ad-id"],                      # Optional
    status=["active", "paused"],           # Optional
    include_archived=False,                # Optional
    limit=50,                              # Optional
    offset=0                               # Optional
)
```

#### Create / Update Ad
```python
ad = client.create_ad(
    adaccount_id="account-id",
    adset_id="adset-id",
    creative_id="creative-id",
    name="Homepage Hero Ad"
)

updated = client.update_ad(ad["id"], name="Homepage Hero Ad v2")
```

### Pixel Endpoints

#### Get Pixels
```python
pixels = client.get_pixels(
    adaccount_id="account-id",  # Required
    limit=50,                   # Optional
    offset=0                    # Optional
)
pixel = client.get_pixel("pixel-id")
events = client.get_pixel_events("pixel-id", limit=100, offset=0)
```

### Audience Management

#### Audience File Format Requirements

When creating or updating audiences with `media_id`, you need to upload a media file containing user data. The file must meet these requirements:

- **File Format**: CSV or TXT files only
- **Structure**: Single column format (one identifier per row)
- **Encoding**: UTF-8 encoding
- **File Size**: Maximum 25MB
- **Content**: 
  - For email audiences: Each row must contain a valid email address
  - For other audience types: Each row contains a single identifier (e.g., IP address, Blockgraph ID, Experian LUID, Liveramp ID)

**Example CSV file for email audiences:**
```csv
user1@example.com
user2@example.com
user3@example.com
```

**Example TXT file for email audiences:**
```
user1@example.com
user2@example.com
user3@example.com
```

**Upload Process:**
1. Upload your file using the `upload_media()` method to get a `media_id`
2. Use the `media_id` when creating an audience or updating audience users
3. The file will be validated automatically

#### Get All Audiences
```python
audiences = client.get_audiences(
    adaccount_id="account-id",       # Required
    name="Audience Name",            # Optional: filter by name
    status="active",                 # Optional: filter by status
    limit=50,                        # Optional: limit results
    offset=0,                        # Optional: pagination offset
    sort="name_asc"                  # Optional: sort order
)
```

#### Get Specific Audience
```python
audience = client.get_audience("audience-id")
```

#### Create Audience
```python
# Option 1: Create audience with media file
# Note: media_id must reference a CSV or TXT file uploaded via upload_media()
# The file must contain one identifier per row (see Audience File Format Requirements above)
audience = client.create_audience(
    adaccount_id="account-id",
    name="Audience Name",
    segment_type="email",              # email, ctv, ip_address, mobile_ad_id, etc.
    media_id="media-id",                # From upload_media() response
    description="Optional description", # Optional
)

# Option 2: Create audience with users list (max 10,000 users)
audience = client.create_audience(
    adaccount_id="account-id",
    name="Audience Name",
    segment_type="email",
    users=["user1@example.com", "user2@example.com"],
    description="Optional description"  # Optional
)
```

#### Update Audience
```python
audience = client.update_audience(
    "audience-id",
    name="Updated Audience Name",
    description="Updated description"
)
```

#### Update Audience Users
```python
# Add users to an audience
client.update_audience_users(
    audience_id="audience-id",
    users=["user1@example.com", "user2@example.com"],
    remove=False  # Set to True to remove users instead
)

# Remove users from an audience
client.update_audience_users(
    audience_id="audience-id",
    users=["user1@example.com"],
    remove=True
)

# Update audience users using uploaded media
client.update_audience_users(
    audience_id="audience-id",
    media_id="new-media-id",
    remove=False
)
```

#### Delete Audience
```python
client.delete_audience("audience-id")
```

### Me Endpoints

#### Get Organizations
```python
organizations = client.get_organizations(
    limit=50,    # Optional: limit results (default: 10, max: 100)
    offset=0     # Optional: pagination offset (default: 0)
)
```

#### Get Ad Accounts
```python
adaccounts = client.get_adaccounts(
    limit=50,                                      # Optional: limit results (default: 10, max: 100)
    offset=0,                                      # Optional: pagination offset (default: 0)
    organization_ids=["org-id-1", "org-id-2"],    # Optional: repeatable organization_id query filters
    authorization_statuses=["authorized"]          # Optional: authorized, not_authorized
)
```

## Error Handling

The SDK provides specific exception types for different error scenarios:

```python
from universal_ads_sdk import UniversalAdsError, AuthenticationError, APIError

try:
    creative = client.create_creative(...)
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
except APIError as e:
    print(f"API error {e.status_code}: {e}")
    print(f"Response data: {e.response_data}")
except UniversalAdsError as e:
    print(f"SDK error: {e}")
```

## Configuration

### Environment Variables

You can also initialize the client using environment variables:

```python
import os
from universal_ads_sdk import UniversalAdsClient

client = UniversalAdsClient(
    api_key=os.getenv("UNIVERSAL_ADS_API_KEY"),
    private_key_pem=os.getenv("UNIVERSAL_ADS_PRIVATE_KEY")
)
```

### Custom Base URL

For testing or development, you can use a custom base URL:

```python
client = UniversalAdsClient(
    api_key="your-api-key",
    private_key_pem="your-private-key",
    base_url="https://staging-api.universalads.com/v1"
)
```

## Authentication

The SDK uses request signing for secure API access. Each request is signed with your private key and includes:

## Requirements

- Python 3.8+
- requests >= 2.25.0
- cryptography >= 3.4.0
- urllib3 >= 1.26.0

## Security

### API Credentials
- **Never commit API credentials to version control**
- Use environment variables for production deployments
- Test files with credentials are excluded from git via `.gitignore`

### Environment Variables (Recommended)
```bash
export UNIVERSAL_ADS_API_KEY="your-api-key"
export UNIVERSAL_ADS_PRIVATE_KEY="your-private-key-pem"
```

```python
import os
from universal_ads_sdk import UniversalAdsClient

client = UniversalAdsClient(
    api_key=os.getenv("UNIVERSAL_ADS_API_KEY"),
    private_key_pem=os.getenv("UNIVERSAL_ADS_PRIVATE_KEY")
)
```

## Development

### Running Tests

The SDK includes test templates in the `tests/` directory. For security, use environment variables:

```bash
# Set up environment variables
cp env.template .env
# Edit .env with your credentials
pip install python-dotenv  # Optional, for better .env support

# Quick test (basic validation)
python tests/test_template.py

# Comprehensive test (all endpoints)
python tests/comprehensive_test.py
```

See `tests/README.md` for detailed testing information.

For development testing with pytest:
```bash
pip install -e ".[dev]"
pytest
```

### Code Formatting

```bash
black universal_ads_sdk/
flake8 universal_ads_sdk/
```

## Support

- Documentation: [https://developers.universalads.com/](https://developers.universalads.com/)
- Issues: [GitHub Issues](https://github.com/universal-ads/universal-ads-sdk-python/issues)
- Email: support+sdk@universalads.com

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
