Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-10-24 - N+1 Network Bottleneck in Printify API Loops
**Learning:** Looping through product blueprints and sequentially fetching variants from third-party APIs causes a massive N+1 network bottleneck. Using `@functools.lru_cache` mitigates this but returning mutable lists poses thread-safety and downstream state mutation risks. Python's `json.dumps()` natively serializes tuples into JSON arrays, allowing tuples to be safely used as drop-in, immutable replacements for lists.
**Action:** Always cast memoized sequence returns to `tuple` and update type hints to `tuple` or `Tuple[...]` when using `lru_cache` to memoize API payloads to ensure thread safety and prevent regressions.
11 changes: 8 additions & 3 deletions create_merch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import json
import time
import os
from typing import List, Dict, Any
import functools
from typing import List, Dict, Any, Tuple
from dotenv import load_dotenv

load_dotenv(os.path.join(os.path.dirname(__file__), '.env'))
Expand Down Expand Up @@ -53,13 +54,17 @@ def get_product_blueprints() -> List[Dict[str, Any]]:
{"product_name": "Tote Bag", "blueprint_id": 1234, "variant_id": None, "color": "Navy", "placement": {"position": "front", "size": 1.5}},
]

def get_variants(blueprint_id: int, print_provider_id: int = 29) -> List[int]:
# Why: Caching this prevents N+1 network requests when creating multiple products with the same blueprint
# Impact: Eliminates redundant HTTP calls, making bulk creation significantly faster
# Safety: Blueprint variants are highly static; safe to cache. Cast to tuple to ensure return is immutable
@functools.lru_cache(maxsize=None)
def get_variants(blueprint_id: int, print_provider_id: int = 29) -> Tuple[int, ...]:
url = f"{BASE_URL}/catalog/blueprints/{blueprint_id}/print_providers/{print_provider_id}/variants.json"
resp = requests.get(url, headers=get_headers())
resp.raise_for_status()
data = resp.json()
variants = data.get("variants", [])
return [v["id"] for v in variants[:3]] if variants else []
return tuple([v["id"] for v in variants[:3]]) if variants else ()

def create_product(blueprint_id: int, variant_ids: List[int], product_name: str,
color: str, placement: Dict[str, Any]) -> Dict[str, Any]:
Expand Down
9 changes: 7 additions & 2 deletions merch_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import time
import os
import functools
from pathlib import Path
from dotenv import load_dotenv

Expand Down Expand Up @@ -39,11 +40,15 @@ def upload_image(image_path: str) -> str:
raise ValueError(f"No image ID returned: {resp.text}")
return image_id

def get_variants(blueprint_id: int, provider_id: int) -> list:
# Why: Caching this prevents N+1 network requests when creating multiple products with the same blueprint
# Impact: Eliminates redundant HTTP calls, making bulk creation significantly faster
# Safety: Blueprint variants are highly static; safe to cache. Cast to tuple to ensure return is immutable
@functools.lru_cache(maxsize=None)
def get_variants(blueprint_id: int, provider_id: int) -> tuple:
url = f"{BASE_URL}/catalog/blueprints/{blueprint_id}/print_providers/{provider_id}/variants.json"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
return [v["id"] for v in resp.json().get("variants", [])[:4]]
return tuple([v["id"] for v in resp.json().get("variants", [])[:4]])

def create_product(title: str, blueprint_id: int, provider_id: int,
variant_ids: list, image_id: str) -> dict:
Expand Down
9 changes: 7 additions & 2 deletions merg_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import time
import os
import functools
from pathlib import Path
from dotenv import load_dotenv

Expand All @@ -23,11 +24,15 @@ def get_blueprints() -> list:
resp.raise_for_status()
return resp.json()

def get_blueprint_variants(blueprint_id: int, provider_id: int) -> list:
# Why: Caching this prevents N+1 network requests when creating multiple products with the same blueprint
# Impact: Eliminates redundant HTTP calls, making bulk creation significantly faster
# Safety: Blueprint variants are highly static; safe to cache. Cast to tuple to ensure return is immutable
@functools.lru_cache(maxsize=None)
def get_blueprint_variants(blueprint_id: int, provider_id: int) -> tuple:
url = f"{BASE_URL}/catalog/blueprints/{blueprint_id}/print_providers/{provider_id}/variants.json"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
return resp.json().get("variants", [])
return tuple(resp.json().get("variants", []))

def upload_image(image_path: str) -> str:
url = f"{BASE_URL}/uploads/images.json"
Expand Down
9 changes: 7 additions & 2 deletions printify_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import json
import time
import requests
import functools
from pathlib import Path
from dotenv import load_dotenv

Expand Down Expand Up @@ -70,11 +71,15 @@ def save_upload_cache(data: dict):
with open(UPLOAD_CACHE, "w") as f:
json.dump(data, f, indent=2)

def get_variants(blueprint_id: int, provider_id: int) -> list:
# Why: Caching this prevents N+1 network requests when creating multiple products with the same blueprint
# Impact: Eliminates redundant HTTP calls, making bulk creation significantly faster
# Safety: Blueprint variants are highly static; safe to cache. Cast to tuple to ensure return is immutable
@functools.lru_cache(maxsize=None)
def get_variants(blueprint_id: int, provider_id: int) -> tuple:
url = f"{BASE_URL}/catalog/blueprints/{blueprint_id}/print_providers/{provider_id}/variants.json"
r = requests.get(url, headers=AUTH_H, timeout=15)
r.raise_for_status()
return [v["id"] for v in r.json().get("variants", [])[:4]]
return tuple([v["id"] for v in r.json().get("variants", [])[:4]])

def upload_image_file(path: str) -> str:
"""Upload a local PNG file to Printify. Returns image ID."""
Expand Down