mycopunk-swag/cli/mycopunk/pod/prodigi.py

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()