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