Theflame47 commited on
Commit
289034d
·
verified ·
1 Parent(s): 540398b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -52
app.py CHANGED
@@ -7,6 +7,7 @@ from typing import Dict, Any, List, Generator, Tuple, Optional
7
  import requests
8
  import gradio as gr
9
  import base64
 
10
 
11
  PRINTIFY_BASE = "https://api.printify.com"
12
  DEFAULT_BASE_PRICE = 0.001
@@ -28,23 +29,86 @@ def _auth_headers() -> Dict[str, str]:
28
  return {"Authorization": f"Bearer {token}"}
29
 
30
 
31
- def _req(method: str, path: str, json_body: Optional[Dict[str, Any]] = None) -> Any:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  kwargs: Dict[str, Any] = {
33
  "headers": _auth_headers(),
34
  "timeout": 60,
35
  }
36
  if json_body is not None:
37
  kwargs["json"] = json_body
38
- r = requests.request(
39
- method,
40
- f"{PRINTIFY_BASE}{path}",
41
- **kwargs,
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):
@@ -53,13 +117,17 @@ def _log(logs: List[str], msg: str):
53
 
54
  def _find_first_valid_pair(logs: List[str]) -> Dict[str, Any]:
55
  _log(logs, "Listing blueprints")
56
- blueprints = _req("GET", "/v1/catalog/blueprints.json")
57
 
58
  for bp in blueprints:
59
  bp_id = str(bp["id"])
60
  _log(logs, f"Blueprint {bp_id}: fetching providers")
61
 
62
- providers = _req("GET", f"/v1/catalog/blueprints/{bp_id}/print_providers.json")
 
 
 
 
63
  for p in providers:
64
  p_id = str(p["id"])
65
  _log(logs, f"Blueprint {bp_id} / Provider {p_id}: fetching variants")
@@ -67,6 +135,7 @@ def _find_first_valid_pair(logs: List[str]) -> Dict[str, Any]:
67
  vr = _req(
68
  "GET",
69
  f"/v1/catalog/blueprints/{bp_id}/print_providers/{p_id}/variants.json?show-out-of-stock=1",
 
70
  )
71
  variants = vr.get("variants")
72
  if isinstance(variants, list) and variants:
@@ -76,17 +145,99 @@ def _find_first_valid_pair(logs: List[str]) -> Dict[str, Any]:
76
  "blueprint": bp,
77
  "provider": p,
78
  "variants": variants,
79
- "blueprintDetails": _req("GET", f"/v1/catalog/blueprints/{bp_id}.json"),
80
- "providerDetails": _req("GET", f"/v1/catalog/print_providers/{p_id}.json"),
 
 
 
 
 
 
 
 
81
  "shippingInfo": _req(
82
  "GET",
83
  f"/v1/catalog/blueprints/{bp_id}/print_providers/{p_id}/shipping.json",
 
84
  ),
85
  }
86
 
87
  raise RuntimeError("No blueprint/provider pair with variants found.")
88
 
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  def _build_product(blob: Dict[str, Any], currency: str, logs: List[str]) -> Dict[str, Any]:
91
  variants = blob["variants"]
92
 
@@ -232,7 +383,7 @@ def _upload_grid_from_file(logs: List[str]) -> Dict[str, Any]:
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"):
238
  raise RuntimeError(f"Unexpected upload response: {str(resp)[:500]}")
@@ -352,7 +503,10 @@ def _create_product_all_variants_with_grid(
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 ""
357
 
358
  payload = {
@@ -374,6 +528,7 @@ def _create_product_all_variants_with_grid(
374
  "POST",
375
  f"/v1/shops/{shop_id}/products.json",
376
  json_body=payload,
 
377
  )
378
  if not isinstance(created, Dict):
379
  raise RuntimeError(f"Unexpected create response: {str(created)[:500]}")
@@ -383,7 +538,7 @@ def _create_product_all_variants_with_grid(
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 []
@@ -450,6 +605,7 @@ def _update_prices_to_cost_plus_margin(
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]}")
@@ -469,7 +625,7 @@ def run(currency: str) -> Generator[Tuple[str, str], None, None]:
469
  _log(logs, "START")
470
  yield flush()
471
 
472
- shops = _req("GET", "/v1/shops.json")
473
  _log(logs, f"SHOP_LIST {json.dumps(shops)}")
474
  yield flush()
475
 
@@ -481,8 +637,6 @@ def run(currency: str) -> Generator[Tuple[str, str], None, None]:
481
  yield flush()
482
 
483
  blob = _find_first_valid_pair(logs)
484
- yield flush()
485
-
486
  result = _build_product(blob, currency or "USD", logs)
487
  _log(logs, "DONE")
488
  yield flush()
@@ -504,7 +658,7 @@ def phase_b(currency: str) -> Generator[Tuple[str, str], None, None]:
504
  _log(logs, "PHASE_B_START")
505
  yield flush()
506
 
507
- shops = _req("GET", "/v1/shops.json")
508
  _log(logs, f"SHOP_LIST {json.dumps(shops)}")
509
  yield flush()
510
 
@@ -512,48 +666,90 @@ def phase_b(currency: str) -> Generator[Tuple[str, str], None, None]:
512
  _log(logs, f"SHOP_ID {shop_id}")
513
  yield flush()
514
 
515
- blob = _find_first_valid_pair(logs)
 
 
 
 
 
 
516
  yield flush()
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
 
@@ -564,7 +760,7 @@ def phase_b(currency: str) -> Generator[Tuple[str, str], None, None]:
564
 
565
 
566
  with gr.Blocks(title="Printify Catalog Probe") as demo:
567
- gr.Markdown("Extract and normalize ONE Printify blueprint/provider into a structured JSON object.")
568
 
569
  currency = gr.Textbox(label="Currency", value="USD")
570
  btn = gr.Button("Run")
 
7
  import requests
8
  import gradio as gr
9
  import base64
10
+ import re
11
 
12
  PRINTIFY_BASE = "https://api.printify.com"
13
  DEFAULT_BASE_PRICE = 0.001
 
29
  return {"Authorization": f"Bearer {token}"}
30
 
31
 
32
+ def _parse_retry_after_seconds(r: requests.Response) -> Optional[float]:
33
+ ra = r.headers.get("Retry-After")
34
+ if ra:
35
+ try:
36
+ return float(ra)
37
+ except Exception:
38
+ pass
39
+
40
+ reset = r.headers.get("X-RateLimit-Reset") or r.headers.get("X-RateLimit-Reset-After")
41
+ if reset:
42
+ try:
43
+ return float(reset)
44
+ except Exception:
45
+ pass
46
+
47
+ try:
48
+ txt = (r.text or "")[:2000]
49
+ except Exception:
50
+ txt = ""
51
+ m = re.search(r"(\d+(\.\d+)?)\s*(s|sec|secs|second|seconds)\b", txt, re.I)
52
+ if m:
53
+ try:
54
+ return float(m.group(1))
55
+ except Exception:
56
+ return None
57
+ return None
58
+
59
+
60
+ def _req(
61
+ method: str,
62
+ path: str,
63
+ json_body: Optional[Dict[str, Any]] = None,
64
+ logs: Optional[List[str]] = None,
65
+ ) -> Any:
66
  kwargs: Dict[str, Any] = {
67
  "headers": _auth_headers(),
68
  "timeout": 60,
69
  }
70
  if json_body is not None:
71
  kwargs["json"] = json_body
72
+
73
+ while True:
74
+ r = requests.request(
75
+ method,
76
+ f"{PRINTIFY_BASE}{path}",
77
+ **kwargs,
78
+ )
79
+
80
+ if r.status_code == 429:
81
+ wait_s = _parse_retry_after_seconds(r)
82
+ if logs is not None:
83
+ _log(
84
+ logs,
85
+ f"RATE_LIMIT 429 path={path} retry_after={wait_s} "
86
+ f"headers={dict(r.headers)} body={r.text[:400]}",
87
+ )
88
+ if wait_s is None:
89
+ raise RuntimeError(f"HTTP 429 without retry instruction: {r.text[:2000]}")
90
+ time.sleep(max(0.0, wait_s))
91
+ continue
92
+
93
+ if r.status_code >= 500:
94
+ wait_s = _parse_retry_after_seconds(r)
95
+ if logs is not None:
96
+ _log(
97
+ logs,
98
+ f"SERVER_ERROR {r.status_code} path={path} retry_after={wait_s} "
99
+ f"body={r.text[:400]}",
100
+ )
101
+ if wait_s is None:
102
+ raise RuntimeError(f"HTTP {r.status_code}: {r.text[:2000]}")
103
+ time.sleep(max(0.0, wait_s))
104
+ continue
105
+
106
+ if r.status_code >= 400:
107
+ raise RuntimeError(f"HTTP {r.status_code}: {r.text[:2000]}")
108
+
109
+ if r.text:
110
+ return r.json()
111
+ return {}
112
 
113
 
114
  def _log(logs: List[str], msg: str):
 
117
 
118
  def _find_first_valid_pair(logs: List[str]) -> Dict[str, Any]:
119
  _log(logs, "Listing blueprints")
120
+ blueprints = _req("GET", "/v1/catalog/blueprints.json", logs=logs)
121
 
122
  for bp in blueprints:
123
  bp_id = str(bp["id"])
124
  _log(logs, f"Blueprint {bp_id}: fetching providers")
125
 
126
+ providers = _req(
127
+ "GET",
128
+ f"/v1/catalog/blueprints/{bp_id}/print_providers.json",
129
+ logs=logs,
130
+ )
131
  for p in providers:
132
  p_id = str(p["id"])
133
  _log(logs, f"Blueprint {bp_id} / Provider {p_id}: fetching variants")
 
135
  vr = _req(
136
  "GET",
137
  f"/v1/catalog/blueprints/{bp_id}/print_providers/{p_id}/variants.json?show-out-of-stock=1",
138
+ logs=logs,
139
  )
140
  variants = vr.get("variants")
141
  if isinstance(variants, list) and variants:
 
145
  "blueprint": bp,
146
  "provider": p,
147
  "variants": variants,
148
+ "blueprintDetails": _req(
149
+ "GET",
150
+ f"/v1/catalog/blueprints/{bp_id}.json",
151
+ logs=logs,
152
+ ),
153
+ "providerDetails": _req(
154
+ "GET",
155
+ f"/v1/catalog/print_providers/{p_id}.json",
156
+ logs=logs,
157
+ ),
158
  "shippingInfo": _req(
159
  "GET",
160
  f"/v1/catalog/blueprints/{bp_id}/print_providers/{p_id}/shipping.json",
161
+ logs=logs,
162
  ),
163
  }
164
 
165
  raise RuntimeError("No blueprint/provider pair with variants found.")
166
 
167
 
168
+ def _get_all_provider_blobs_for_blueprint(
169
+ logs: List[str],
170
+ blueprint: Dict[str, Any],
171
+ ) -> List[Dict[str, Any]]:
172
+ bp_id = str(blueprint.get("id"))
173
+ if not bp_id:
174
+ raise RuntimeError("Blueprint object missing id.")
175
+
176
+ _log(logs, f"BLUEPRINT_ALL_PROVIDERS blueprint_id={bp_id}: fetching providers")
177
+ providers = _req(
178
+ "GET",
179
+ f"/v1/catalog/blueprints/{bp_id}/print_providers.json",
180
+ logs=logs,
181
+ )
182
+ if not isinstance(providers, list) or not providers:
183
+ raise RuntimeError(f"No providers found for blueprint {bp_id}.")
184
+
185
+ bp_details = _req(
186
+ "GET",
187
+ f"/v1/catalog/blueprints/{bp_id}.json",
188
+ logs=logs,
189
+ )
190
+
191
+ blobs: List[Dict[str, Any]] = []
192
+ for p in providers:
193
+ if not isinstance(p, dict):
194
+ continue
195
+ p_id = str(p.get("id"))
196
+ if not p_id:
197
+ continue
198
+
199
+ _log(logs, f"Blueprint {bp_id} / Provider {p_id}: fetching variants (ALL)")
200
+ vr = _req(
201
+ "GET",
202
+ f"/v1/catalog/blueprints/{bp_id}/print_providers/{p_id}/variants.json?show-out-of-stock=1",
203
+ logs=logs,
204
+ )
205
+ variants = vr.get("variants")
206
+ count = len(variants) if isinstance(variants, list) else 0
207
+ _log(
208
+ logs,
209
+ f"PROVIDER_VARIANTS blueprint={bp_id} provider={p_id} count={count}",
210
+ )
211
+ if not isinstance(variants, list) or not variants:
212
+ continue
213
+
214
+ provider_details = _req(
215
+ "GET",
216
+ f"/v1/catalog/print_providers/{p_id}.json",
217
+ logs=logs,
218
+ )
219
+ shipping_info = _req(
220
+ "GET",
221
+ f"/v1/catalog/blueprints/{bp_id}/print_providers/{p_id}/shipping.json",
222
+ logs=logs,
223
+ )
224
+
225
+ blobs.append(
226
+ {
227
+ "blueprint": blueprint,
228
+ "provider": p,
229
+ "variants": variants,
230
+ "blueprintDetails": bp_details,
231
+ "providerDetails": provider_details,
232
+ "shippingInfo": shipping_info,
233
+ }
234
+ )
235
+
236
+ if not blobs:
237
+ raise RuntimeError(f"No providers with variants for blueprint {bp_id}.")
238
+ return blobs
239
+
240
+
241
  def _build_product(blob: Dict[str, Any], currency: str, logs: List[str]) -> Dict[str, Any]:
242
  variants = blob["variants"]
243
 
 
383
  }
384
 
385
  _log(logs, f"UPLOADING_GRID path={path} bytes={len(raw)} b64len={len(b64)}")
386
+ resp = _req("POST", "/v1/uploads/images.json", json_body=payload, logs=logs)
387
 
388
  if not isinstance(resp, dict) or not resp.get("id"):
389
  raise RuntimeError(f"Unexpected upload response: {str(resp)[:500]}")
 
503
  if not print_areas_payload:
504
  raise RuntimeError("No placeholders payload generated for any variant.")
505
 
506
+ provider_details = blob.get("providerDetails") or {}
507
+ provider_name = provider_details.get("title") or provider_details.get("name") or str(provider_id)
508
+
509
+ title = (product_info.get("name") or "Printify Grid Test") + f" — {provider_name}"
510
  description = product_info.get("description") or ""
511
 
512
  payload = {
 
528
  "POST",
529
  f"/v1/shops/{shop_id}/products.json",
530
  json_body=payload,
531
+ logs=logs,
532
  )
533
  if not isinstance(created, Dict):
534
  raise RuntimeError(f"Unexpected create response: {str(created)[:500]}")
 
538
 
539
 
540
  def _get_product(logs: List[str], shop_id: str, product_id: str) -> Dict[str, Any]:
541
+ prod = _req("GET", f"/v1/shops/{shop_id}/products/{product_id}.json", logs=logs)
542
  if not isinstance(prod, dict):
543
  raise RuntimeError(f"Unexpected product response: {str(prod)[:500]}")
544
  variants = prod.get("variants") or []
 
605
  "PUT",
606
  f"/v1/shops/{shop_id}/products/{product_id}.json",
607
  json_body={"variants": payload_variants},
608
+ logs=logs,
609
  )
610
  if not isinstance(upd, dict):
611
  raise RuntimeError(f"Unexpected update response: {str(upd)[:500]}")
 
625
  _log(logs, "START")
626
  yield flush()
627
 
628
+ shops = _req("GET", "/v1/shops.json", logs=logs)
629
  _log(logs, f"SHOP_LIST {json.dumps(shops)}")
630
  yield flush()
631
 
 
637
  yield flush()
638
 
639
  blob = _find_first_valid_pair(logs)
 
 
640
  result = _build_product(blob, currency or "USD", logs)
641
  _log(logs, "DONE")
642
  yield flush()
 
658
  _log(logs, "PHASE_B_START")
659
  yield flush()
660
 
661
+ shops = _req("GET", "/v1/shops.json", logs=logs)
662
  _log(logs, f"SHOP_LIST {json.dumps(shops)}")
663
  yield flush()
664
 
 
666
  _log(logs, f"SHOP_ID {shop_id}")
667
  yield flush()
668
 
669
+ first_blob = _find_first_valid_pair(logs)
670
+ bp_obj = first_blob.get("blueprint") or {}
671
+ bp_id = bp_obj.get("id")
672
+ if bp_id is None:
673
+ raise RuntimeError("First valid blob missing blueprint id.")
674
+ _log(logs, f"PHASE_B_BLUEPRINT {bp_id}")
675
+ result["phaseBBlueprintId"] = bp_id
676
  yield flush()
677
 
678
+ provider_blobs = _get_all_provider_blobs_for_blueprint(logs, bp_obj)
679
+ result["providerCount"] = len(provider_blobs)
680
+ _log(logs, f"PHASE_B_PROVIDER_COUNT {len(provider_blobs)}")
681
  yield flush()
682
 
683
  upload = _upload_grid_from_file(logs)
684
  result["gridUpload"] = upload
685
  yield flush()
686
 
687
+ provider_runs: List[Dict[str, Any]] = []
688
+ currency_code = currency or "USD"
689
+
690
+ for idx, blob in enumerate(provider_blobs):
691
+ provider_details = blob.get("providerDetails") or {}
692
+ provider_meta = blob.get("provider") or {}
693
+ provider_id = provider_meta.get("id")
694
+ provider_name = (
695
+ provider_details.get("title")
696
+ or provider_details.get("name")
697
+ or str(provider_id)
698
+ )
699
 
700
+ _log(
701
+ logs,
702
+ f"PROVIDER_RUN index={idx} provider_id={provider_id} "
703
+ f"name={provider_name}",
704
+ )
705
+ yield flush()
706
 
707
+ product_info = _build_product(blob, currency_code, logs)
708
+ _log_catalog_variants(logs, product_info, limit=75)
709
+ yield flush()
710
 
711
+ created = _create_product_all_variants_with_grid(
712
+ logs,
713
+ shop_id=shop_id,
714
+ blob=blob,
715
+ product_info=product_info,
716
+ upload=upload,
717
+ )
718
+ created_id = str(created.get("id") or "")
719
+ if not created_id:
720
+ raise RuntimeError("Created product response missing id.")
721
+ yield flush()
722
 
723
+ prod1 = _get_product(logs, shop_id, created_id)
724
+ yield flush()
725
+
726
+ _log(
727
+ logs,
728
+ f"COMPARE_COUNTS provider={provider_id} "
729
+ f"catalog={len(product_info.get('variants') or [])} "
730
+ f"shop={len(prod1.get('variants') or [])}",
731
+ )
732
+ yield flush()
733
+
734
+ upd = _update_prices_to_cost_plus_margin(logs, shop_id, created_id, prod1)
735
+ yield flush()
736
+
737
+ prod2 = _get_product(logs, shop_id, created_id)
738
+ yield flush()
739
+
740
+ provider_runs.append(
741
+ {
742
+ "providerId": provider_id,
743
+ "providerName": provider_name,
744
+ "catalogVariantCount": len(product_info.get("variants") or []),
745
+ "shopVariantCountAfterCreate": len(prod1.get("variants") or []),
746
+ "shopVariantCountFinal": len(prod2.get("variants") or []),
747
+ "productId": created_id,
748
+ "priceUpdateResponse": upd,
749
+ }
750
+ )
751
 
752
+ result["providerRuns"] = provider_runs
 
753
  _log(logs, "PHASE_B_DONE")
754
  yield flush()
755
 
 
760
 
761
 
762
  with gr.Blocks(title="Printify Catalog Probe") as demo:
763
+ gr.Markdown("Extract and normalize ONE Printify blueprint into provider-specific JSON objects and products.")
764
 
765
  currency = gr.Textbox(label="Currency", value="USD")
766
  btn = gr.Button("Run")