diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..4005212 --- /dev/null +++ b/.jules/bolt.md @@ -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. diff --git a/create_merch.py b/create_merch.py index d81fc54..b3f1088 100644 --- a/create_merch.py +++ b/create_merch.py @@ -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')) @@ -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]: diff --git a/merch_factory.py b/merch_factory.py index 748c8d2..dbcf534 100644 --- a/merch_factory.py +++ b/merch_factory.py @@ -7,6 +7,7 @@ import json import time import os +import functools from pathlib import Path from dotenv import load_dotenv @@ -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: diff --git a/merg_bridge.py b/merg_bridge.py index 0142922..b0e51f3 100644 --- a/merg_bridge.py +++ b/merg_bridge.py @@ -6,6 +6,7 @@ import json import time import os +import functools from pathlib import Path from dotenv import load_dotenv @@ -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" diff --git a/printify_manager.py b/printify_manager.py index c781cf4..c6a93e3 100644 --- a/printify_manager.py +++ b/printify_manager.py @@ -20,6 +20,7 @@ import json import time import requests +import functools from pathlib import Path from dotenv import load_dotenv @@ -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."""