Theflame47 commited on
Commit
edce36b
·
verified ·
1 Parent(s): 5237ea9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -202
app.py CHANGED
@@ -9,7 +9,6 @@ import gradio as gr
9
  import base64
10
 
11
  PRINTIFY_BASE = "https://api.printify.com"
12
- DEFAULT_BASE_PRICE = 24.99
13
  GRID_FILENAME = "grid.png"
14
 
15
 
@@ -42,9 +41,7 @@ def _req(method: str, path: str, json_body: Optional[Dict[str, Any]] = None) ->
42
  )
43
  if r.status_code >= 400:
44
  raise RuntimeError(f"HTTP {r.status_code}: {r.text[:2000]}")
45
- if r.text:
46
- return r.json()
47
- return {}
48
 
49
 
50
  def _log(logs: List[str], msg: str):
@@ -92,7 +89,9 @@ def _build_product(blob: Dict[str, Any], currency: str, logs: List[str]) -> Dict
92
 
93
  snapshot = []
94
  for v in variants:
95
- cents = v.get("price")
 
 
96
  cents = int(cents) if isinstance(cents, (int, str)) and str(cents).isdigit() else None
97
 
98
  ph = v.get("placeholders")
@@ -114,8 +113,8 @@ def _build_product(blob: Dict[str, Any], currency: str, logs: List[str]) -> Dict
114
  "sku": v.get("sku"),
115
  "size": (v.get("options") or {}).get("size"),
116
  "color": (v.get("options") or {}).get("color"),
117
- "priceCents": cents,
118
- "price": round(cents / 100, 2) if cents is not None else None,
119
  "placeholders": placeholders,
120
  })
121
 
@@ -151,8 +150,10 @@ def _build_product(blob: Dict[str, Any], currency: str, logs: List[str]) -> Dict
151
  if ph.get("position")
152
  })
153
 
154
- all_cents = [v["priceCents"] for v in snapshot if v["priceCents"] is not None]
155
- min_price = round(min(all_cents) / 100, 2) if all_cents else DEFAULT_BASE_PRICE
 
 
156
 
157
  colors = sorted({v["color"] for v in snapshot if v["color"]})
158
  sizes = sorted({v["size"] for v in snapshot if v["size"]})
@@ -197,23 +198,20 @@ def _pick_shop_id(shops_resp: Any) -> str:
197
  if not isinstance(items, list) or not items:
198
  raise RuntimeError("No shops found on /v1/shops.json for this token.")
199
 
200
- forced = (os.environ.get("PRINTIFY_SHOP_ID") or "").strip()
201
- if forced:
202
- for s in items:
203
- if isinstance(s, dict) and str(s.get("id")) == forced:
204
- return str(s["id"])
205
- raise RuntimeError(f"Preferred shop '{forced}' not found; refusing to fallback.")
206
-
207
- preferred_name = os.environ.get("PRINTIFY_SHOP_NAME", "Atheria").strip().lower()
208
 
209
  for s in items:
210
  if not isinstance(s, dict):
211
  continue
212
- title = (s.get("title") or s.get("name") or "").strip().lower()
213
- if title and title == preferred_name:
 
 
 
 
214
  return str(s["id"])
215
 
216
- raise RuntimeError(f"Preferred shop name '{os.environ.get('PRINTIFY_SHOP_NAME','Atheria')}' not found; refusing to fallback.")
217
 
218
 
219
  def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
@@ -222,16 +220,14 @@ def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
222
  raise RuntimeError(f"Grid image not found at {path}")
223
 
224
  with open(path, "rb") as f:
225
- raw = f.read()
226
-
227
- b64 = base64.b64encode(raw).decode("ascii")
228
 
229
  payload = {
230
  "file_name": GRID_FILENAME,
231
  "contents": b64,
232
  }
233
 
234
- _log(logs, f"UPLOADING_GRID path={path} bytes={len(raw)} b64len={len(b64)}")
235
  resp = _req("POST", "/v1/uploads/images.json", json_body=payload)
236
 
237
  if not isinstance(resp, dict) or not resp.get("id"):
@@ -244,6 +240,29 @@ def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
244
  )
245
  return resp
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  def _scale_fill(ph_w: float, ph_h: float, img_w: float, img_h: float) -> float:
249
  if ph_w <= 0 or ph_h <= 0 or img_w <= 0 or img_h <= 0:
@@ -254,33 +273,13 @@ def _scale_fill(ph_w: float, ph_h: float, img_w: float, img_h: float) -> float:
254
  return float(s)
255
 
256
 
257
- def _log_catalog_variants(logs: List[str], product_info: Dict[str, Any], limit: int = 50):
258
- variants = product_info.get("variants") or []
259
- total = len(variants) if isinstance(variants, list) else 0
260
- _log(logs, f"CATALOG_VARIANTS_TOTAL {total}")
261
- if not isinstance(variants, list):
262
- return
263
- n = 0
264
- for v in variants:
265
- if not isinstance(v, dict):
266
- continue
267
- vid = v.get("id")
268
- size = v.get("size")
269
- color = v.get("color")
270
- ph = v.get("placeholders") or []
271
- phn = len(ph) if isinstance(ph, list) else 0
272
- _log(logs, f"CATALOG_VARIANT id={vid} size={size} color={color} placeholders={phn}")
273
- n += 1
274
- if n >= limit:
275
- break
276
-
277
-
278
- def _create_product_all_variants_with_grid(
279
  logs: List[str],
280
  shop_id: str,
281
  blob: Dict[str, Any],
282
  product_info: Dict[str, Any],
283
  upload: Dict[str, Any],
 
284
  ) -> Dict[str, Any]:
285
  bp_id = (blob.get("blueprint") or {}).get("id")
286
  provider_id = (blob.get("provider") or {}).get("id")
@@ -291,66 +290,49 @@ def _create_product_all_variants_with_grid(
291
  if not isinstance(variants, list) or not variants:
292
  raise RuntimeError("No variants to create product with.")
293
 
 
 
 
 
 
 
294
  img_id = upload.get("id")
295
  img_w = float(upload.get("width") or 0)
296
  img_h = float(upload.get("height") or 0)
297
 
298
- variants_payload = []
299
- print_areas_payload = []
300
-
301
- for v in variants:
302
- if not isinstance(v, dict):
303
  continue
304
- vid = v.get("id")
305
- if vid is None:
 
 
306
  continue
307
-
308
- variants_payload.append({
309
- "id": int(vid),
310
- "price": 1,
311
- "is_enabled": True,
312
- })
313
-
314
- placeholders = v.get("placeholders") or []
315
- if not isinstance(placeholders, list) or not placeholders:
316
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- ph_payload = []
319
- for ph in placeholders:
320
- if not isinstance(ph, dict):
321
- continue
322
- pos = ph.get("position")
323
- pw = ph.get("width")
324
- phh = ph.get("height")
325
- if not pos or pw is None or phh is None:
326
- continue
327
- try:
328
- pwf = float(pw)
329
- phf = float(phh)
330
- except Exception:
331
- continue
332
- scale = _scale_fill(pwf, phf, img_w, img_h)
333
- ph_payload.append({
334
- "position": pos,
335
- "images": [{
336
- "id": img_id,
337
- "x": 0.5,
338
- "y": 0.5,
339
- "scale": scale,
340
- "angle": 0,
341
- }],
342
- })
343
-
344
- if ph_payload:
345
- print_areas_payload.append({
346
- "variant_ids": [int(vid)],
347
- "placeholders": ph_payload,
348
- })
349
-
350
- if not variants_payload:
351
- raise RuntimeError("No valid variant ids to create product with.")
352
- if not print_areas_payload:
353
- raise RuntimeError("No placeholders payload generated for any variant.")
354
 
355
  title = product_info.get("name") or "Printify Grid Test"
356
  description = product_info.get("description") or ""
@@ -360,14 +342,22 @@ def _create_product_all_variants_with_grid(
360
  "description": description,
361
  "blueprint_id": int(bp_id),
362
  "print_provider_id": int(provider_id),
363
- "variants": variants_payload,
364
- "print_areas": print_areas_payload,
 
 
 
 
 
 
 
365
  }
366
 
367
  _log(
368
  logs,
369
- f"PHASE_B_CREATE shop_id={shop_id} blueprint_id={bp_id} provider_id={provider_id} "
370
- f"variants_payload={len(variants_payload)} print_areas_payload={len(print_areas_payload)}",
 
371
  )
372
 
373
  created = _req(
@@ -382,82 +372,6 @@ def _create_product_all_variants_with_grid(
382
  return created
383
 
384
 
385
- def _get_product(logs: List[str], shop_id: str, product_id: str) -> Dict[str, Any]:
386
- prod = _req("GET", f"/v1/shops/{shop_id}/products/{product_id}.json")
387
- if not isinstance(prod, dict):
388
- raise RuntimeError(f"Unexpected product response: {str(prod)[:500]}")
389
- variants = prod.get("variants") or []
390
- _log(logs, f"SHOP_VARIANTS_TOTAL {len(variants) if isinstance(variants, list) else 0}")
391
- if isinstance(variants, list):
392
- for v in variants:
393
- if not isinstance(v, dict):
394
- continue
395
- _log(
396
- logs,
397
- f"SHOP_VARIANT id={v.get('id')} enabled={v.get('is_enabled')} "
398
- f"cost={v.get('cost')} price={v.get('price')}",
399
- )
400
- return prod
401
-
402
-
403
- def _margin_pct() -> float:
404
- raw = (os.environ.get("PRINTIFY_MARGIN_PCT") or "").strip()
405
- if not raw:
406
- return 0.0
407
- try:
408
- return float(raw)
409
- except Exception:
410
- return 0.0
411
-
412
-
413
- def _update_prices_to_cost_plus_margin(
414
- logs: List[str],
415
- shop_id: str,
416
- product_id: str,
417
- product: Dict[str, Any],
418
- ) -> Dict[str, Any]:
419
- variants = product.get("variants") or []
420
- if not isinstance(variants, list) or not variants:
421
- raise RuntimeError("No variants found on created product for price update.")
422
-
423
- m = _margin_pct()
424
- payload_variants = []
425
- for v in variants:
426
- if not isinstance(v, dict):
427
- continue
428
- vid = v.get("id")
429
- cost = v.get("cost")
430
- if vid is None or cost is None:
431
- continue
432
- try:
433
- cost_cents = int(cost)
434
- except Exception:
435
- continue
436
- price_cents = int(round(cost_cents * (1.0 + m)))
437
- if price_cents < 1:
438
- price_cents = 1
439
- payload_variants.append({
440
- "id": int(vid),
441
- "price": price_cents,
442
- "is_enabled": True,
443
- })
444
- _log(logs, f"PRICE_SET id={vid} cost={cost_cents} price={price_cents} margin_pct={m}")
445
-
446
- if not payload_variants:
447
- raise RuntimeError("No variant prices could be computed from costs.")
448
-
449
- upd = _req(
450
- "PUT",
451
- f"/v1/shops/{shop_id}/products/{product_id}.json",
452
- json_body={"variants": payload_variants},
453
- )
454
- if not isinstance(upd, dict):
455
- raise RuntimeError(f"Unexpected update response: {str(upd)[:500]}")
456
-
457
- _log(logs, f"PRICE_UPDATE_DONE variants={len(payload_variants)}")
458
- return upd
459
-
460
-
461
  def run(currency: str) -> Generator[Tuple[str, str], None, None]:
462
  logs: List[str] = []
463
  result: Dict[str, Any] = {}
@@ -517,43 +431,21 @@ def phase_b(currency: str) -> Generator[Tuple[str, str], None, None]:
517
 
518
  product_info = _build_product(blob, currency or "USD", logs)
519
  result["phaseAProduct"] = product_info
520
- _log_catalog_variants(logs, product_info, limit=75)
521
  yield flush()
522
 
523
  upload = _upload_grid_from_file(logs)
524
  result["gridUpload"] = upload
525
  yield flush()
526
 
527
- created = _create_product_all_variants_with_grid(
528
  logs,
529
  shop_id=shop_id,
530
  blob=blob,
531
  product_info=product_info,
532
  upload=upload,
 
533
  )
534
  result["createdProduct"] = created
535
- yield flush()
536
-
537
- created_id = str(created.get("id") or "")
538
- if not created_id:
539
- raise RuntimeError("Created product response missing id.")
540
-
541
- prod1 = _get_product(logs, shop_id, created_id)
542
- result["createdProductReadback"] = prod1
543
- yield flush()
544
-
545
- _log(
546
- logs,
547
- f"COMPARE_COUNTS catalog={len(product_info.get('variants') or [])} shop={len(prod1.get('variants') or [])}",
548
- )
549
- yield flush()
550
-
551
- upd = _update_prices_to_cost_plus_margin(logs, shop_id, created_id, prod1)
552
- result["priceUpdateResponse"] = upd
553
- yield flush()
554
-
555
- prod2 = _get_product(logs, shop_id, created_id)
556
- result["finalProductReadback"] = prod2
557
  _log(logs, "PHASE_B_DONE")
558
  yield flush()
559
 
 
9
  import base64
10
 
11
  PRINTIFY_BASE = "https://api.printify.com"
 
12
  GRID_FILENAME = "grid.png"
13
 
14
 
 
41
  )
42
  if r.status_code >= 400:
43
  raise RuntimeError(f"HTTP {r.status_code}: {r.text[:2000]}")
44
+ return r.json()
 
 
45
 
46
 
47
  def _log(logs: List[str], msg: str):
 
89
 
90
  snapshot = []
91
  for v in variants:
92
+ cents = v.get("cost")
93
+ if cents is None:
94
+ cents = v.get("price")
95
  cents = int(cents) if isinstance(cents, (int, str)) and str(cents).isdigit() else None
96
 
97
  ph = v.get("placeholders")
 
113
  "sku": v.get("sku"),
114
  "size": (v.get("options") or {}).get("size"),
115
  "color": (v.get("options") or {}).get("color"),
116
+ "costCents": cents,
117
+ "cost": round(cents / 100, 2) if cents is not None else None,
118
  "placeholders": placeholders,
119
  })
120
 
 
150
  if ph.get("position")
151
  })
152
 
153
+ all_cost_cents = [v["costCents"] for v in snapshot if v["costCents"] is not None]
154
+ if not all_cost_cents:
155
+ raise RuntimeError("No variant cost returned by API (expected 'cost' or 'price' in cents).")
156
+ min_price = round(min(all_cost_cents) / 100, 2)
157
 
158
  colors = sorted({v["color"] for v in snapshot if v["color"]})
159
  sizes = sorted({v["size"] for v in snapshot if v["size"]})
 
198
  if not isinstance(items, list) or not items:
199
  raise RuntimeError("No shops found on /v1/shops.json for this token.")
200
 
201
+ preferred_name = os.environ.get("PRINTIFY_SHOP_NAME", "Atheria")
 
 
 
 
 
 
 
202
 
203
  for s in items:
204
  if not isinstance(s, dict):
205
  continue
206
+ title = s.get("title") or s.get("name")
207
+ if title and title.strip().lower() == preferred_name.strip().lower():
208
+ return str(s["id"])
209
+
210
+ for s in items:
211
+ if isinstance(s, dict) and s.get("id"):
212
  return str(s["id"])
213
 
214
+ raise RuntimeError("Could not pick a shop id from response.")
215
 
216
 
217
  def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
 
220
  raise RuntimeError(f"Grid image not found at {path}")
221
 
222
  with open(path, "rb") as f:
223
+ b64 = base64.b64encode(f.read()).decode("ascii")
 
 
224
 
225
  payload = {
226
  "file_name": GRID_FILENAME,
227
  "contents": b64,
228
  }
229
 
230
+ _log(logs, f"UPLOADING_GRID path={path} bytes={len(b64)}")
231
  resp = _req("POST", "/v1/uploads/images.json", json_body=payload)
232
 
233
  if not isinstance(resp, dict) or not resp.get("id"):
 
240
  )
241
  return resp
242
 
243
+ with open(path, "rb") as f:
244
+ r = requests.post(
245
+ f"{PRINTIFY_BASE}/v1/uploads/images.json",
246
+ headers=_auth_headers(),
247
+ files={"file": (GRID_FILENAME, f, "image/png")},
248
+ timeout=60,
249
+ )
250
+
251
+ if r.status_code >= 400:
252
+ raise RuntimeError(f"UPLOAD_HTTP {r.status_code}: {r.text[:2000]}")
253
+
254
+ resp = r.json()
255
+ if not isinstance(resp, dict) or not resp.get("id"):
256
+ raise RuntimeError(f"Unexpected upload response: {str(resp)[:500]}")
257
+
258
+ _log(
259
+ logs,
260
+ f"GRID_UPLOAD id={resp.get('id')} "
261
+ f"file={resp.get('file_name') or resp.get('name')} "
262
+ f"w={resp.get('width')} h={resp.get('height')}",
263
+ )
264
+ return resp
265
+
266
 
267
  def _scale_fill(ph_w: float, ph_h: float, img_w: float, img_h: float) -> float:
268
  if ph_w <= 0 or ph_h <= 0 or img_w <= 0 or img_h <= 0:
 
273
  return float(s)
274
 
275
 
276
+ def _create_one_product_with_grid(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  logs: List[str],
278
  shop_id: str,
279
  blob: Dict[str, Any],
280
  product_info: Dict[str, Any],
281
  upload: Dict[str, Any],
282
+ currency: str,
283
  ) -> Dict[str, Any]:
284
  bp_id = (blob.get("blueprint") or {}).get("id")
285
  provider_id = (blob.get("provider") or {}).get("id")
 
290
  if not isinstance(variants, list) or not variants:
291
  raise RuntimeError("No variants to create product with.")
292
 
293
+ v = variants[0]
294
+ variant_id = v.get("id")
295
+ if variant_id is None:
296
+ raise RuntimeError("Variant missing id.")
297
+
298
+ placeholders = v.get("placeholders") or []
299
  img_id = upload.get("id")
300
  img_w = float(upload.get("width") or 0)
301
  img_h = float(upload.get("height") or 0)
302
 
303
+ ph_payload = []
304
+ for ph in placeholders:
305
+ if not isinstance(ph, dict):
 
 
306
  continue
307
+ pos = ph.get("position")
308
+ pw = ph.get("width")
309
+ phh = ph.get("height")
310
+ if not pos or pw is None or phh is None:
311
  continue
312
+ try:
313
+ pwf = float(pw)
314
+ phf = float(phh)
315
+ except Exception:
 
 
 
 
 
316
  continue
317
+ scale = _scale_fill(pwf, phf, img_w, img_h)
318
+ ph_payload.append({
319
+ "position": pos,
320
+ "images": [{
321
+ "id": img_id,
322
+ "x": 0.5,
323
+ "y": 0.5,
324
+ "scale": scale,
325
+ "angle": 0,
326
+ }],
327
+ })
328
+ _log(
329
+ logs,
330
+ f"FILL_PLACEHOLDER pos={pos} ph={int(pwf)}x{int(phf)} "
331
+ f"img={int(img_w)}x{int(img_h)} scale={round(scale, 6)}",
332
+ )
333
 
334
+ if not ph_payload:
335
+ raise RuntimeError("No placeholders for chosen variant.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  title = product_info.get("name") or "Printify Grid Test"
338
  description = product_info.get("description") or ""
 
342
  "description": description,
343
  "blueprint_id": int(bp_id),
344
  "print_provider_id": int(provider_id),
345
+ "variants": [{
346
+ "id": int(variant_id),
347
+ "price": int(product_info.get("price") * 100),
348
+ "is_enabled": True,
349
+ }],
350
+ "print_areas": [{
351
+ "variant_ids": [int(variant_id)],
352
+ "placeholders": ph_payload,
353
+ }],
354
  }
355
 
356
  _log(
357
  logs,
358
+ f"PHASE_B_CREATE shop_id={shop_id} "
359
+ f"blueprint_id={bp_id} provider_id={provider_id} variant_id={variant_id} "
360
+ f"price_cents={int(product_info.get('price') * 100)}",
361
  )
362
 
363
  created = _req(
 
372
  return created
373
 
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  def run(currency: str) -> Generator[Tuple[str, str], None, None]:
376
  logs: List[str] = []
377
  result: Dict[str, Any] = {}
 
431
 
432
  product_info = _build_product(blob, currency or "USD", logs)
433
  result["phaseAProduct"] = product_info
 
434
  yield flush()
435
 
436
  upload = _upload_grid_from_file(logs)
437
  result["gridUpload"] = upload
438
  yield flush()
439
 
440
+ created = _create_one_product_with_grid(
441
  logs,
442
  shop_id=shop_id,
443
  blob=blob,
444
  product_info=product_info,
445
  upload=upload,
446
+ currency=currency or "USD",
447
  )
448
  result["createdProduct"] = created
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  _log(logs, "PHASE_B_DONE")
450
  yield flush()
451