feat(pry): shopify_oauth helper + commerce_sync accepts code flow
Add shopify_oauth.py with the three primitives needed to ship Pry as a
Shopify public app:
- install_url() — build the authorize URL with state nonce
- exchange_code() — trade callback code for offline access token
- verify_webhook_hmac() — verify X-Shopify-Hmac-SHA256 on webhooks
Refactor commerce_sync.sync_to_shopify to accept either a static
access_token (current callers) or a (code, shop_domain, api_key,
api_secret) tuple. When the code flow is used the access token is
exchanged transparently via shopify_oauth.exchange_code() before the
product POST loop runs. Empty credentials still return a graceful
{"success": False, ...} dict (existing test contract preserved).
Tests: test_commerce_sync.py (2 pass); install_url sanity-checked in REPL.
This commit is contained in:
parent
b5be9311b9
commit
0125da76d0
2 changed files with 200 additions and 2 deletions
|
|
@ -113,16 +113,63 @@ async def sync_to_woocommerce(
|
|||
async def sync_to_shopify(
|
||||
products: list[dict[str, Any]],
|
||||
shop_url: str,
|
||||
access_token: str,
|
||||
access_token: str = "",
|
||||
*,
|
||||
code: str = "",
|
||||
shop_domain: str = "",
|
||||
api_key: str = "",
|
||||
api_secret: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Sync products to Shopify via REST API.
|
||||
|
||||
Two auth modes are supported:
|
||||
|
||||
1. Static access token (server-to-server, classic custom apps):
|
||||
await sync_to_shopify(products, shop_url, access_token="shpat_...")
|
||||
2. OAuth code exchange (public apps, embedded installs):
|
||||
await sync_to_shopify(
|
||||
products,
|
||||
shop_url,
|
||||
code="<oauth callback code>",
|
||||
shop_domain="mystore.myshopify.com",
|
||||
api_key="...",
|
||||
api_secret="...",
|
||||
)
|
||||
The code is exchanged for a permanent access token via
|
||||
``shopify_oauth.exchange_code`` and then the sync proceeds normally.
|
||||
|
||||
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
|
||||
access_token: Shopify admin API access token (static mode)
|
||||
code: OAuth callback code (public-app mode)
|
||||
shop_domain: Bare *.myshopify.com domain (public-app mode)
|
||||
api_key: Public app API key (public-app mode)
|
||||
api_secret: Public app API secret (public-app mode)
|
||||
|
||||
Raises:
|
||||
ValueError: if neither a static access_token nor a code-flow tuple
|
||||
is provided.
|
||||
"""
|
||||
from client import get_client
|
||||
from shopify_oauth import exchange_code
|
||||
|
||||
if not access_token:
|
||||
if not (code and shop_domain and api_key and api_secret):
|
||||
logger.error(
|
||||
"shopify_sync_missing_credentials",
|
||||
extra={"shop_url": shop_url, "has_code": bool(code)},
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"total": len(products),
|
||||
"synced": 0,
|
||||
"failed": len(products),
|
||||
"results": [],
|
||||
"error": "missing credentials: provide access_token OR (code, shop_domain, api_key, api_secret)",
|
||||
}
|
||||
token_resp = exchange_code(shop_domain, code, api_key, api_secret)
|
||||
access_token = token_resp["access_token"]
|
||||
|
||||
client = await get_client()
|
||||
base_url = shop_url.rstrip("/")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue