pryscraper/commerce_sync.py
cryptorugmunch a7c30b12cd
Some checks failed
CI / lint (push) Failing after 2s
CI / typecheck (push) Failing after 2s
CI / test (push) Failing after 2s
CI / Secret scan (gitleaks) (push) Failing after 1s
CI / Security audit (bandit) (push) Failing after 2s
chore(lint): auto-fix 253 of 283 ruff issues (F401, I001, E402, RUF100, UP037, SIM105)
Mass ruff auto-fix:
  - ruff check --fix: 109 issues fixed (F401 unused imports,
    I001 unsorted imports, UP037 quoted annotations, SIM105
    suppressible exception, RUF100 unused-noqa)
  - ruff check --fix --unsafe-fixes: 22 additional issues
  - ruff format: 70 files reformatted
  - Manual pass: fix 16 misplaced import httpx lines
  - Manual pass: fix remaining E402 (import-after-docstring)

Result: 283 errors -> 30 errors.

The remaining 30 are real issues that need manual review:
  5 F401 unused-import (likely auto-generated stubs)
  5 F821 undefined-name (real bugs in code that references
    redis/pydantic/LLMRegistry without imports)
  3 BLE001 (the compliance LLM fallback is intentional; the
    other two are real)
  3 RUF012 mutable-class-default
  3 SIM105, 3 SIM117, 2 E722, 2 E741
  1 B007, 1 B025, 1 E402, 1 RUF200 (pyproject.toml issue)

Tests: 436/437 pass (1 pre-existing SSE sandbox failure).
format check + import sort: now clean.
make ci: still gated on the 30 remaining real issues.
Follow-up: triage the 30 issues file-by-file.
2026-07-02 21:51:25 +02:00

198 lines
6.6 KiB
Python

"""Pry — Commerce Platform Sync Engine.
Unified interface for WooCommerce, Shopify, and generic API sync."""
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Rug Munch Media LLC
#
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
# Licensed under MIT. See LICENSE.
import logging
from typing import Any
import httpx
from paths import PRY_DATA_DIR
logger = logging.getLogger(__name__)
# ── Record sync operation ──
COMMERCE_DIR = PRY_DATA_DIR / "commerce"
COMMERCE_DIR.mkdir(parents=True, exist_ok=True)
async def sync_to_woocommerce(
products: list[dict[str, Any]],
wp_url: str,
consumer_key: str,
consumer_secret: str,
category_id: int = 0,
status: str = "draft",
) -> dict[str, Any]:
"""Sync products to WooCommerce via REST API.
Args:
products: List of product dicts with name, price, description, image_url
wp_url: WordPress site URL (e.g., https://mystore.com)
consumer_key: WooCommerce REST API consumer key
consumer_secret: WooCommerce REST API consumer secret
category_id: Product category ID to assign
status: Product status (draft, publish, pending)
"""
from client import get_client
client = await get_client()
base_url = wp_url.rstrip("/")
auth = (consumer_key, consumer_secret)
results = []
for product in products:
wc_product = {
"name": product.get("name", "Unnamed Product")[:255],
"regular_price": str(product.get("price", 0)),
"description": (product.get("description") or product.get("content", ""))[:5000],
"short_description": (product.get("short_description") or "")[:500],
"images": [{"src": product["image_url"]}] if product.get("image_url") else [],
"categories": [{"id": category_id}] if category_id else [],
"status": status,
"meta_data": [
{"key": "_pry_source_url", "value": product.get("url", "")},
{
"key": "_pry_imported_at",
"value": __import__("datetime")
.datetime.now(__import__("datetime").timezone.utc)
.isoformat(),
},
],
}
try:
resp = await client.post(
f"{base_url}/wp-json/wc/v3/products",
json=wc_product,
auth=auth,
timeout=30,
)
if resp.is_success:
data = resp.json()
results.append(
{
"success": True,
"woo_id": data.get("id"),
"name": data.get("name"),
"edit_url": f"{base_url}/wp-admin/post.php?post={data.get('id')}&action=edit",
}
)
else:
results.append(
{
"success": False,
"name": product.get("name", "Unknown"),
"error": f"WooCommerce error {resp.status_code}: {resp.text[:200]}",
}
)
except (httpx.HTTPError, httpx.RequestError) as e:
results.append(
{
"success": False,
"name": product.get("name", "Unknown"),
"error": str(e)[:200],
}
)
success_count = sum(1 for r in results if r["success"])
return {
"success": success_count > 0,
"total": len(products),
"synced": success_count,
"failed": len(products) - success_count,
"results": results,
}
async def sync_to_shopify(
products: list[dict[str, Any]],
shop_url: str,
access_token: str,
) -> dict[str, Any]:
"""Sync products to Shopify via REST API.
Args:
products: List of product dicts with name, price, description, image_url
shop_url: Shopify store URL (e.g., https://mystore.myshopify.com)
access_token: Shopify admin API access token
"""
from client import get_client
client = await get_client()
base_url = shop_url.rstrip("/")
results = []
for product in products:
shopify_product = {
"product": {
"title": (product.get("name") or "Unnamed Product")[:255],
"body_html": f"<div>{(product.get('description') or product.get('content', ''))[:5000]}</div>",
"vendor": "Pry Import",
"product_type": "Reference",
"status": "draft",
"variants": [
{
"price": str(product.get("price", 0)),
"inventory_management": "shopify",
}
],
"metafields": [
{
"key": "pry_source_url",
"value": product.get("url", ""),
"type": "single_line_text_field",
"namespace": "pry",
}
],
}
}
try:
resp = await client.post(
f"{base_url}/admin/api/2024-01/products.json",
json=shopify_product,
headers={"X-Shopify-Access-Token": access_token},
timeout=30,
)
if resp.is_success:
data = resp.json()
pid = data.get("product", {}).get("id")
results.append(
{
"success": True,
"shopify_id": pid,
"name": data.get("product", {}).get("title"),
"edit_url": f"{base_url}/admin/products/{pid}",
}
)
else:
results.append(
{
"success": False,
"name": product.get("name", "Unknown"),
"error": f"Shopify error {resp.status_code}: {resp.text[:200]}",
}
)
except (httpx.HTTPError, httpx.RequestError) as e:
results.append(
{
"success": False,
"name": product.get("name", "Unknown"),
"error": str(e)[:200],
}
)
success_count = sum(1 for r in results if r["success"])
return {
"success": success_count > 0,
"total": len(products),
"synced": success_count,
"failed": len(products) - success_count,
"results": results,
}