276 lines
8.2 KiB
Python
276 lines
8.2 KiB
Python
"""
|
|
Prodigi API integration.
|
|
|
|
API Documentation: https://www.prodigi.com/print-api/docs/
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import httpx
|
|
|
|
from .base import PODProvider, ProductResult, MockupResult
|
|
|
|
|
|
class ProdigiProvider(PODProvider):
|
|
"""Prodigi print-on-demand provider."""
|
|
|
|
name = "prodigi"
|
|
|
|
SANDBOX_URL = "https://api.sandbox.prodigi.com/v4.0"
|
|
PRODUCTION_URL = "https://api.prodigi.com/v4.0"
|
|
|
|
def __init__(self, api_key: str, sandbox: bool = False) -> None:
|
|
super().__init__(api_key, sandbox)
|
|
base_url = self.SANDBOX_URL if sandbox else self.PRODUCTION_URL
|
|
self.client = httpx.AsyncClient(
|
|
base_url=base_url,
|
|
headers={
|
|
"X-API-Key": api_key,
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout=30.0,
|
|
)
|
|
|
|
def _validate_credentials(self) -> None:
|
|
"""Validate Prodigi API key."""
|
|
if not self.api_key:
|
|
raise ValueError("Prodigi API key is required")
|
|
|
|
async def create_product(
|
|
self,
|
|
design_path: Path,
|
|
metadata: dict,
|
|
product_config: dict,
|
|
) -> ProductResult:
|
|
"""
|
|
Create a product template on Prodigi.
|
|
|
|
Note: Prodigi doesn't have a "product" concept like Printful.
|
|
Instead, orders are placed directly with product SKUs and image URLs.
|
|
This method creates a "template" in our system for tracking.
|
|
"""
|
|
try:
|
|
# Upload the design file
|
|
file_url = await self.upload_file(design_path)
|
|
if not file_url:
|
|
return ProductResult(
|
|
success=False,
|
|
error="Failed to upload design file"
|
|
)
|
|
|
|
# Prodigi works on-demand, so we just validate the SKU exists
|
|
sku = product_config.get("sku")
|
|
|
|
# Validate SKU by checking catalog
|
|
catalog = await self.get_catalog()
|
|
valid_skus = [p.get("sku") for p in catalog]
|
|
|
|
if sku and sku not in valid_skus:
|
|
return ProductResult(
|
|
success=False,
|
|
error=f"Invalid SKU: {sku}"
|
|
)
|
|
|
|
# Return a "virtual" product ID for tracking
|
|
product_id = f"prodigi_{metadata.get('slug')}_{sku}"
|
|
|
|
return ProductResult(
|
|
success=True,
|
|
product_id=product_id,
|
|
data={
|
|
"sku": sku,
|
|
"image_url": file_url,
|
|
"name": metadata.get("name"),
|
|
"retail_price": product_config.get("retail_price"),
|
|
},
|
|
)
|
|
|
|
except Exception as e:
|
|
return ProductResult(success=False, error=str(e))
|
|
|
|
async def update_product(
|
|
self,
|
|
product_id: str,
|
|
design_path: Path,
|
|
metadata: dict,
|
|
) -> ProductResult:
|
|
"""Update product by re-uploading the design file."""
|
|
try:
|
|
file_url = await self.upload_file(design_path)
|
|
if not file_url:
|
|
return ProductResult(
|
|
success=False,
|
|
error="Failed to upload design file"
|
|
)
|
|
|
|
return ProductResult(
|
|
success=True,
|
|
product_id=product_id,
|
|
data={"image_url": file_url},
|
|
)
|
|
|
|
except Exception as e:
|
|
return ProductResult(success=False, error=str(e))
|
|
|
|
async def generate_mockup(
|
|
self,
|
|
product_id: str,
|
|
variant: Optional[str] = None,
|
|
) -> MockupResult:
|
|
"""
|
|
Generate product mockup.
|
|
|
|
Note: Prodigi doesn't have a built-in mockup generator.
|
|
This would need to use a third-party service or custom solution.
|
|
"""
|
|
return MockupResult(
|
|
success=False,
|
|
error="Prodigi does not provide mockup generation. Use external mockup service."
|
|
)
|
|
|
|
async def get_product(self, product_id: str) -> Optional[dict]:
|
|
"""
|
|
Get product details.
|
|
|
|
Since Prodigi doesn't store products, this returns None.
|
|
Product data should be stored locally in metadata.yaml.
|
|
"""
|
|
return None
|
|
|
|
async def list_products(self, limit: int = 100) -> list[dict]:
|
|
"""
|
|
List products.
|
|
|
|
Since Prodigi doesn't store products, returns empty list.
|
|
Use local design list instead.
|
|
"""
|
|
return []
|
|
|
|
async def get_catalog(self, product_type: Optional[str] = None) -> list[dict]:
|
|
"""Get Prodigi product catalog."""
|
|
try:
|
|
response = await self.client.get("/products")
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
products = data.get("products", [])
|
|
|
|
if product_type:
|
|
# Filter by type
|
|
type_keywords = {
|
|
"sticker": ["sticker", "sti"],
|
|
"print": ["print", "poster", "art"],
|
|
"canvas": ["canvas"],
|
|
}
|
|
keywords = type_keywords.get(product_type, [product_type])
|
|
products = [
|
|
p for p in products
|
|
if any(kw in p.get("sku", "").lower() or
|
|
kw in p.get("description", "").lower()
|
|
for kw in keywords)
|
|
]
|
|
|
|
return products
|
|
|
|
except Exception:
|
|
return []
|
|
|
|
async def upload_file(self, file_path: Path) -> Optional[str]:
|
|
"""
|
|
Upload a file to Prodigi.
|
|
|
|
Note: Prodigi expects image URLs, not uploaded files.
|
|
You need to host the image somewhere accessible.
|
|
|
|
Options:
|
|
1. Use Cloudflare R2/S3
|
|
2. Use a public URL from your server
|
|
3. Use Prodigi's asset upload (if available)
|
|
"""
|
|
# For now, return None - in production, implement actual upload
|
|
# to your hosting service (R2, S3, etc.)
|
|
return None
|
|
|
|
async def create_order(
|
|
self,
|
|
sku: str,
|
|
image_url: str,
|
|
recipient: dict,
|
|
quantity: int = 1,
|
|
) -> dict:
|
|
"""
|
|
Create an order on Prodigi.
|
|
|
|
Args:
|
|
sku: Product SKU
|
|
image_url: URL to the design image
|
|
recipient: Shipping address dict
|
|
quantity: Number of items
|
|
|
|
Returns:
|
|
Order data from Prodigi
|
|
"""
|
|
try:
|
|
payload = {
|
|
"shippingMethod": "Standard",
|
|
"recipient": recipient,
|
|
"items": [
|
|
{
|
|
"sku": sku,
|
|
"copies": quantity,
|
|
"assets": [
|
|
{
|
|
"printArea": "default",
|
|
"url": image_url,
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
response = await self.client.post("/orders", json=payload)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
async def get_order(self, order_id: str) -> Optional[dict]:
|
|
"""Get order status from Prodigi."""
|
|
try:
|
|
response = await self.client.get(f"/orders/{order_id}")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except Exception:
|
|
return None
|
|
|
|
async def get_quote(
|
|
self,
|
|
sku: str,
|
|
destination_country: str = "US",
|
|
quantity: int = 1,
|
|
) -> Optional[dict]:
|
|
"""Get a price quote for a product."""
|
|
try:
|
|
payload = {
|
|
"shippingMethod": "Standard",
|
|
"destinationCountryCode": destination_country,
|
|
"items": [
|
|
{
|
|
"sku": sku,
|
|
"copies": quantity,
|
|
}
|
|
],
|
|
}
|
|
|
|
response = await self.client.post("/quotes", json=payload)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
except Exception:
|
|
return None
|
|
|
|
async def close(self) -> None:
|
|
"""Close the HTTP client."""
|
|
await self.client.aclose()
|