Theflame47 commited on
Commit
af2e202
·
verified ·
1 Parent(s): e6478c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +194 -83
app.py CHANGED
@@ -9,7 +9,7 @@ import gradio as gr
9
  import base64
10
 
11
  PRINTIFY_BASE = "https://api.printify.com"
12
- DEFAULT_BASE_PRICE = 0.01
13
  GRID_FILENAME = "grid.png"
14
 
15
 
@@ -42,7 +42,9 @@ 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
- return r.json()
 
 
46
 
47
 
48
  def _log(logs: List[str], msg: str):
@@ -195,10 +197,12 @@ def _pick_shop_id(shops_resp: Any) -> str:
195
  if not isinstance(items, list) or not items:
196
  raise RuntimeError("No shops found on /v1/shops.json for this token.")
197
 
198
- # Hard override wins (set this in HF Secrets)
199
- forced = os.environ.get("PRINTIFY_SHOP_ID")
200
  if forced:
201
- return str(forced)
 
 
 
202
 
203
  preferred_name = os.environ.get("PRINTIFY_SHOP_NAME", "Atheria").strip().lower()
204
 
@@ -206,10 +210,11 @@ def _pick_shop_id(shops_resp: Any) -> str:
206
  if not isinstance(s, dict):
207
  continue
208
  title = (s.get("title") or s.get("name") or "").strip().lower()
209
- if title == preferred_name:
210
  return str(s["id"])
211
 
212
- raise RuntimeError(f"Preferred shop '{preferred_name}' not found; refusing to fallback.")
 
213
 
214
  def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
215
  path = os.path.join(os.getcwd(), GRID_FILENAME)
@@ -217,14 +222,16 @@ def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
217
  raise RuntimeError(f"Grid image not found at {path}")
218
 
219
  with open(path, "rb") as f:
220
- b64 = base64.b64encode(f.read()).decode("ascii")
 
 
221
 
222
  payload = {
223
  "file_name": GRID_FILENAME,
224
  "contents": b64,
225
  }
226
 
227
- _log(logs, f"UPLOADING_GRID path={path} bytes={len(b64)}")
228
  resp = _req("POST", "/v1/uploads/images.json", json_body=payload)
229
 
230
  if not isinstance(resp, dict) or not resp.get("id"):
@@ -237,47 +244,43 @@ def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
237
  )
238
  return resp
239
 
240
- with open(path, "rb") as f:
241
- r = requests.post(
242
- f"{PRINTIFY_BASE}/v1/uploads/images.json",
243
- headers=_auth_headers(),
244
- files={"file": (GRID_FILENAME, f, "image/png")},
245
- timeout=60,
246
- )
247
-
248
- if r.status_code >= 400:
249
- raise RuntimeError(f"UPLOAD_HTTP {r.status_code}: {r.text[:2000]}")
250
-
251
- resp = r.json()
252
- if not isinstance(resp, dict) or not resp.get("id"):
253
- raise RuntimeError(f"Unexpected upload response: {str(resp)[:500]}")
254
-
255
- _log(
256
- logs,
257
- f"GRID_UPLOAD id={resp.get('id')} "
258
- f"file={resp.get('file_name') or resp.get('name')} "
259
- f"w={resp.get('width')} h={resp.get('height')}",
260
- )
261
- return resp
262
-
263
 
264
  def _scale_fill(ph_w: float, ph_h: float, img_w: float, img_h: float) -> float:
265
  if ph_w <= 0 or ph_h <= 0 or img_w <= 0 or img_h <= 0:
266
  return 1.0
267
- # width-based scale; bump if height would underfill
268
  s = (ph_h * img_w) / (ph_w * img_h)
269
  if s < 1.0:
270
  s = 1.0
271
  return float(s)
272
 
273
 
274
- def _create_one_product_with_grid(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  logs: List[str],
276
  shop_id: str,
277
  blob: Dict[str, Any],
278
  product_info: Dict[str, Any],
279
  upload: Dict[str, Any],
280
- currency: str,
281
  ) -> Dict[str, Any]:
282
  bp_id = (blob.get("blueprint") or {}).get("id")
283
  provider_id = (blob.get("provider") or {}).get("id")
@@ -288,49 +291,66 @@ def _create_one_product_with_grid(
288
  if not isinstance(variants, list) or not variants:
289
  raise RuntimeError("No variants to create product with.")
290
 
291
- v = variants[0]
292
- variant_id = v.get("id")
293
- if variant_id is None:
294
- raise RuntimeError("Variant missing id.")
295
-
296
- placeholders = v.get("placeholders") or []
297
  img_id = upload.get("id")
298
  img_w = float(upload.get("width") or 0)
299
  img_h = float(upload.get("height") or 0)
300
 
301
- ph_payload = []
302
- for ph in placeholders:
303
- if not isinstance(ph, dict):
304
- continue
305
- pos = ph.get("position")
306
- pw = ph.get("width")
307
- phh = ph.get("height")
308
- if not pos or pw is None or phh is None:
309
  continue
310
- try:
311
- pwf = float(pw)
312
- phf = float(phh)
313
- except Exception:
314
  continue
315
- scale = _scale_fill(pwf, phf, img_w, img_h)
316
- ph_payload.append({
317
- "position": pos,
318
- "images": [{
319
- "id": img_id,
320
- "x": 0.5,
321
- "y": 0.5,
322
- "scale": scale,
323
- "angle": 0,
324
- }],
325
  })
326
- _log(
327
- logs,
328
- f"FILL_PLACEHOLDER pos={pos} ph={int(pwf)}x{int(phf)} "
329
- f"img={int(img_w)}x{int(img_h)} scale={round(scale, 6)}",
330
- )
331
 
332
- if not ph_payload:
333
- raise RuntimeError("No placeholders for chosen variant.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
  title = product_info.get("name") or "Printify Grid Test"
336
  description = product_info.get("description") or ""
@@ -340,21 +360,14 @@ def _create_one_product_with_grid(
340
  "description": description,
341
  "blueprint_id": int(bp_id),
342
  "print_provider_id": int(provider_id),
343
- "variants": [{
344
- "id": int(variant_id),
345
- "price": int(product_info.get("price", DEFAULT_BASE_PRICE) * 100),
346
- "is_enabled": True,
347
- }],
348
- "print_areas": [{
349
- "variant_ids": [int(variant_id)],
350
- "placeholders": ph_payload,
351
- }],
352
  }
353
 
354
  _log(
355
  logs,
356
- f"PHASE_B_CREATE shop_id={shop_id} "
357
- f"blueprint_id={bp_id} provider_id={provider_id} variant_id={variant_id}",
358
  )
359
 
360
  created = _req(
@@ -369,6 +382,82 @@ def _create_one_product_with_grid(
369
  return created
370
 
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  def run(currency: str) -> Generator[Tuple[str, str], None, None]:
373
  logs: List[str] = []
374
  result: Dict[str, Any] = {}
@@ -428,21 +517,43 @@ def phase_b(currency: str) -> Generator[Tuple[str, str], None, None]:
428
 
429
  product_info = _build_product(blob, currency or "USD", logs)
430
  result["phaseAProduct"] = product_info
 
431
  yield flush()
432
 
433
  upload = _upload_grid_from_file(logs)
434
  result["gridUpload"] = upload
435
  yield flush()
436
 
437
- created = _create_one_product_with_grid(
438
  logs,
439
  shop_id=shop_id,
440
  blob=blob,
441
  product_info=product_info,
442
  upload=upload,
443
- currency=currency or "USD",
444
  )
445
  result["createdProduct"] = created
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  _log(logs, "PHASE_B_DONE")
447
  yield flush()
448
 
 
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
  )
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):
 
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
 
 
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]:
220
  path = os.path.join(os.getcwd(), GRID_FILENAME)
 
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
  )
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:
250
  return 1.0
 
251
  s = (ph_h * img_w) / (ph_w * img_h)
252
  if s < 1.0:
253
  s = 1.0
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
  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
  "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
  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
 
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