Add examples for feed task management and inventory publishing workflows
This commit is contained in:
parent
2c6bd35ebb
commit
904f4e487e
3 changed files with 363 additions and 6 deletions
|
|
@ -115,6 +115,20 @@ Use the generated model packages for request and response types:
|
|||
|
||||
This section maps typical seller workflows to wrapper methods.
|
||||
|
||||
Quick source jumps:
|
||||
|
||||
- facade: [ebay_client/client.py](../ebay_client/client.py)
|
||||
- notification wrapper: [ebay_client/notification/client.py](../ebay_client/notification/client.py)
|
||||
- inventory wrapper: [ebay_client/inventory/client.py](../ebay_client/inventory/client.py)
|
||||
- fulfillment wrapper: [ebay_client/fulfillment/client.py](../ebay_client/fulfillment/client.py)
|
||||
- account wrapper: [ebay_client/account/client.py](../ebay_client/account/client.py)
|
||||
- feed wrapper: [ebay_client/feed/client.py](../ebay_client/feed/client.py)
|
||||
- media wrapper: [ebay_client/media/client.py](../ebay_client/media/client.py)
|
||||
- FastAPI webhook example: [examples/fastapi_notification_webhook.py](../examples/fastapi_notification_webhook.py)
|
||||
- media workflows example: [examples/media_workflows.py](../examples/media_workflows.py)
|
||||
- inventory publish example: [examples/inventory_publish_flow.py](../examples/inventory_publish_flow.py)
|
||||
- feed task example: [examples/feed_task_flow.py](../examples/feed_task_flow.py)
|
||||
|
||||
### Notification: manage webhook subscriptions
|
||||
|
||||
Typical flow:
|
||||
|
|
@ -143,7 +157,7 @@ Recommended server flow:
|
|||
3. verify the POST body with `WebhookSignatureValidator`
|
||||
4. dispatch the parsed `WebhookEventEnvelope`
|
||||
|
||||
Reference implementation: `examples/fastapi_notification_webhook.py`.
|
||||
Reference implementation: [examples/fastapi_notification_webhook.py](../examples/fastapi_notification_webhook.py).
|
||||
|
||||
### Inventory: read listings and inventory records
|
||||
|
||||
|
|
@ -305,9 +319,157 @@ Helpers:
|
|||
- `VideoWorkflowResult`
|
||||
- `DocumentWorkflowResult`
|
||||
|
||||
Reference implementation: `examples/media_workflows.py`.
|
||||
Reference implementation: [examples/media_workflows.py](../examples/media_workflows.py).
|
||||
|
||||
## 5. Working With Generated Models
|
||||
## 5. End-to-End Recipes
|
||||
|
||||
These are the shortest practical paths through the client for the most common seller tasks.
|
||||
|
||||
### Recipe: read existing listed inventory data
|
||||
|
||||
Use this when you mainly want to inspect what is already listed or staged in eBay.
|
||||
|
||||
```python
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
|
||||
client = EbayClient(
|
||||
EbayOAuthConfig(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
default_scopes=["https://api.ebay.com/oauth/api_scope/sell.inventory.readonly"],
|
||||
)
|
||||
)
|
||||
|
||||
items = client.inventory.get_inventory_items(limit=50)
|
||||
offers = client.inventory.get_offers(limit=50)
|
||||
```
|
||||
|
||||
Methods involved:
|
||||
|
||||
- `get_inventory_items()`
|
||||
- `get_offers()`
|
||||
- optionally `get_inventory_item()` and `get_offer()` for detail reads
|
||||
|
||||
### Recipe: create an inventory item, create an offer, and publish it
|
||||
|
||||
Use this when you want the standard listing flow through the Inventory API.
|
||||
|
||||
```python
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.generated.inventory.models import InventoryItem, OfferDetailsWithKeys
|
||||
|
||||
client = EbayClient(
|
||||
EbayOAuthConfig(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
default_scopes=["https://api.ebay.com/oauth/api_scope/sell.inventory"],
|
||||
)
|
||||
)
|
||||
|
||||
client.inventory.create_or_replace_inventory_item(
|
||||
"SKU-123",
|
||||
InventoryItem(),
|
||||
content_language="en-US",
|
||||
)
|
||||
|
||||
offer = client.inventory.create_offer(OfferDetailsWithKeys())
|
||||
client.inventory.publish_offer(offer.offerId)
|
||||
```
|
||||
|
||||
Operational notes:
|
||||
|
||||
- you typically need business policy IDs and location data in place before the offer is valid
|
||||
- the wrapper requires `content_language` for the item write call because eBay expects it on several write endpoints
|
||||
- the exact request model fields depend on your marketplace and listing format
|
||||
|
||||
### Recipe: receive Notification webhook events in FastAPI
|
||||
|
||||
Use the built-in webhook helpers instead of hand-parsing the signature format.
|
||||
|
||||
Flow:
|
||||
|
||||
1. create a normal `EbayClient`
|
||||
2. build `WebhookPublicKeyResolver` from `client.notification.get_public_key`
|
||||
3. build `WebhookSignatureValidator`
|
||||
4. use `WebhookRequestHandler` for both GET challenge and POST notification handling
|
||||
|
||||
Working example: [examples/fastapi_notification_webhook.py](../examples/fastapi_notification_webhook.py).
|
||||
|
||||
### Recipe: upload a document or video asset for listings
|
||||
|
||||
Use the Media wrapper helpers when you want the client to handle the create, upload, and polling loop for you.
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.generated.media.models import CreateDocumentRequest
|
||||
|
||||
client = EbayClient(
|
||||
EbayOAuthConfig(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
default_scopes=["https://api.ebay.com/oauth/api_scope/sell.inventory"],
|
||||
)
|
||||
)
|
||||
|
||||
result = client.media.create_upload_and_wait_document_from_path(
|
||||
CreateDocumentRequest(
|
||||
documentType="USER_GUIDE_OR_MANUAL",
|
||||
languages=["en-US"],
|
||||
),
|
||||
Path("./manual.pdf"),
|
||||
timeout_seconds=60.0,
|
||||
)
|
||||
|
||||
print(result.document_id)
|
||||
print(result.document.documentStatus)
|
||||
```
|
||||
|
||||
Working example: [examples/media_workflows.py](../examples/media_workflows.py).
|
||||
|
||||
### Recipe: create a Feed task, upload its file, and download the result
|
||||
|
||||
Use this when the operation is file-based and asynchronous.
|
||||
|
||||
Flow:
|
||||
|
||||
1. create the task with `create_task()`
|
||||
2. extract the returned `resource_id`
|
||||
3. upload the task input file with `upload_file()`
|
||||
4. poll `get_task()` until the task reaches the state you expect
|
||||
5. download the result with `get_result_file()`
|
||||
|
||||
This wrapper already provides typed helpers for the file downloads through `FeedFileDownload`.
|
||||
|
||||
Working example: [examples/feed_task_flow.py](../examples/feed_task_flow.py).
|
||||
|
||||
### Recipe: dispute handling and evidence upload
|
||||
|
||||
Use the Fulfillment dispute methods only when you have the dispute-specific scope.
|
||||
|
||||
Flow:
|
||||
|
||||
1. read the dispute with `get_payment_dispute()`
|
||||
2. upload the binary evidence file with `upload_evidence_file()`
|
||||
3. attach or update the evidence metadata with `add_evidence()` or `update_evidence()`
|
||||
4. contest or accept with `contest_payment_dispute()` or `accept_payment_dispute()`
|
||||
|
||||
Caveat: these calls go through the alternate `apiz.ebay.com` fulfillment base URL inside the wrapper.
|
||||
|
||||
### Recipe: use the included examples directly
|
||||
|
||||
The repository now includes these runnable example scripts:
|
||||
|
||||
- [examples/inventory_publish_flow.py](../examples/inventory_publish_flow.py): create or replace an inventory item, create an offer, and publish it
|
||||
- [examples/feed_task_flow.py](../examples/feed_task_flow.py): create a feed task, upload an input file, poll its status, and optionally save the result file
|
||||
- [examples/media_workflows.py](../examples/media_workflows.py): image, document, and video asset workflows
|
||||
- [examples/fastapi_notification_webhook.py](../examples/fastapi_notification_webhook.py): FastAPI-based challenge and notification handling
|
||||
|
||||
## 6. Working With Generated Models
|
||||
|
||||
The wrappers are intentionally thin. Request payloads and response types come from the generated Pydantic model packages.
|
||||
|
||||
|
|
@ -337,14 +499,14 @@ result = client.media.create_upload_and_wait_document_from_path(
|
|||
|
||||
For any wrapper method that takes a payload, look up the corresponding generated request model in the matching `ebay_client.generated.<api>.models` package.
|
||||
|
||||
## 6. Operational Notes
|
||||
## 7. Operational Notes
|
||||
|
||||
- The default token store is process-local and in-memory only.
|
||||
- Empty `202` and `204` responses are normalized by the shared transport so wrapper methods can return `None` cleanly for no-body success cases.
|
||||
- Multipart uploads are already handled by the transport and wrappers for Media, Feed, and dispute evidence uploads.
|
||||
- Absolute URL handling is already built into the transport so wrappers can call alternate eBay hosts when required.
|
||||
|
||||
## 7. Validation and Regeneration
|
||||
## 8. Validation and Regeneration
|
||||
|
||||
Run tests:
|
||||
|
||||
|
|
|
|||
95
examples/feed_task_flow.py
Normal file
95
examples/feed_task_flow.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from time import monotonic, sleep
|
||||
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.generated.feed.models import CreateTaskRequest
|
||||
|
||||
|
||||
TERMINAL_TASK_STATUSES = {
|
||||
"COMPLETED",
|
||||
"COMPLETED_WITH_ERROR",
|
||||
"FAILED",
|
||||
"CANCELLED",
|
||||
}
|
||||
|
||||
|
||||
def require_env(name: str) -> str:
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
raise RuntimeError(f"Environment variable {name} is required")
|
||||
return value
|
||||
|
||||
|
||||
def build_client() -> EbayClient:
|
||||
oauth_config = EbayOAuthConfig(
|
||||
client_id=require_env("EBAY_CLIENT_ID"),
|
||||
client_secret=require_env("EBAY_CLIENT_SECRET"),
|
||||
default_scopes=[os.environ.get("EBAY_FEED_SCOPE", "https://api.ebay.com/oauth/api_scope/sell.fulfillment")],
|
||||
)
|
||||
return EbayClient(oauth_config)
|
||||
|
||||
|
||||
def wait_for_task(client: EbayClient, task_id: str, *, timeout_seconds: float = 120.0, poll_interval_seconds: float = 3.0):
|
||||
deadline = monotonic() + timeout_seconds
|
||||
last_status: str | None = None
|
||||
|
||||
while monotonic() < deadline:
|
||||
task = client.feed.get_task(task_id)
|
||||
last_status = task.status
|
||||
print("task_status:", last_status)
|
||||
if last_status in TERMINAL_TASK_STATUSES:
|
||||
return task
|
||||
sleep(poll_interval_seconds)
|
||||
|
||||
raise TimeoutError(f"Feed task {task_id} did not complete within {timeout_seconds} seconds; last status was {last_status}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = build_client()
|
||||
marketplace_id = os.environ.get("EBAY_MARKETPLACE_ID", "EBAY_US")
|
||||
feed_type = os.environ.get("EBAY_FEED_TYPE", "LMS_ORDER_REPORT")
|
||||
schema_version = os.environ.get("EBAY_FEED_SCHEMA_VERSION", "1.0")
|
||||
upload_path = Path(require_env("EBAY_FEED_UPLOAD_FILE"))
|
||||
result_output_path = os.environ.get("EBAY_FEED_RESULT_OUTPUT")
|
||||
|
||||
created = client.feed.create_task(
|
||||
CreateTaskRequest(feedType=feed_type, schemaVersion=schema_version),
|
||||
marketplace_id=marketplace_id,
|
||||
)
|
||||
print("task_id:", created.resource_id)
|
||||
|
||||
task_id = created.resource_id
|
||||
if not task_id:
|
||||
raise RuntimeError("create_task did not return a task id")
|
||||
|
||||
client.feed.upload_file(
|
||||
task_id,
|
||||
file_name=upload_path.name,
|
||||
content=upload_path.read_bytes(),
|
||||
content_type=os.environ.get("EBAY_FEED_UPLOAD_CONTENT_TYPE", "application/xml"),
|
||||
)
|
||||
print("uploaded_file:", upload_path.name)
|
||||
|
||||
task = wait_for_task(
|
||||
client,
|
||||
task_id,
|
||||
timeout_seconds=float(os.environ.get("EBAY_FEED_TIMEOUT_SECONDS", "120")),
|
||||
poll_interval_seconds=float(os.environ.get("EBAY_FEED_POLL_INTERVAL_SECONDS", "3")),
|
||||
)
|
||||
print("final_task_status:", task.status)
|
||||
|
||||
if task.status in {"COMPLETED", "COMPLETED_WITH_ERROR"}:
|
||||
result_file = client.feed.get_result_file(task_id)
|
||||
print("result_file_name:", result_file.file_name)
|
||||
print("result_file_bytes:", len(result_file.content))
|
||||
if result_output_path:
|
||||
Path(result_output_path).write_bytes(result_file.content)
|
||||
print("result_saved_to:", result_output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
100
examples/inventory_publish_flow.py
Normal file
100
examples/inventory_publish_flow.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.generated.inventory.models import (
|
||||
Amount,
|
||||
Availability,
|
||||
EbayOfferDetailsWithKeys,
|
||||
InventoryItem,
|
||||
ListingPolicies,
|
||||
PricingSummary,
|
||||
Product,
|
||||
ShipToLocationAvailability,
|
||||
)
|
||||
|
||||
|
||||
def require_env(name: str) -> str:
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
raise RuntimeError(f"Environment variable {name} is required")
|
||||
return value
|
||||
|
||||
|
||||
def build_client() -> EbayClient:
|
||||
oauth_config = EbayOAuthConfig(
|
||||
client_id=require_env("EBAY_CLIENT_ID"),
|
||||
client_secret=require_env("EBAY_CLIENT_SECRET"),
|
||||
default_scopes=["https://api.ebay.com/oauth/api_scope/sell.inventory"],
|
||||
)
|
||||
return EbayClient(oauth_config)
|
||||
|
||||
|
||||
def build_inventory_item() -> InventoryItem:
|
||||
return InventoryItem(
|
||||
condition="NEW",
|
||||
availability=Availability(
|
||||
shipToLocationAvailability=ShipToLocationAvailability(quantity=int(os.environ.get("EBAY_AVAILABLE_QUANTITY", "1")))
|
||||
),
|
||||
product=Product(
|
||||
title=require_env("EBAY_ITEM_TITLE"),
|
||||
description=require_env("EBAY_ITEM_DESCRIPTION"),
|
||||
imageUrls=[require_env("EBAY_ITEM_IMAGE_URL")],
|
||||
aspects='{"Brand":["Demo Brand"],"Type":["Demo Item"]}',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def build_offer_payload(sku: str) -> EbayOfferDetailsWithKeys:
|
||||
return EbayOfferDetailsWithKeys(
|
||||
sku=sku,
|
||||
marketplaceId=os.environ.get("EBAY_MARKETPLACE_ID", "EBAY_US"),
|
||||
format="FIXED_PRICE",
|
||||
availableQuantity=int(os.environ.get("EBAY_AVAILABLE_QUANTITY", "1")),
|
||||
categoryId=require_env("EBAY_CATEGORY_ID"),
|
||||
merchantLocationKey=require_env("EBAY_MERCHANT_LOCATION_KEY"),
|
||||
listingDescription=os.environ.get("EBAY_LISTING_DESCRIPTION") or require_env("EBAY_ITEM_DESCRIPTION"),
|
||||
listingDuration="GTC",
|
||||
listingPolicies=ListingPolicies(
|
||||
fulfillmentPolicyId=require_env("EBAY_FULFILLMENT_POLICY_ID"),
|
||||
paymentPolicyId=require_env("EBAY_PAYMENT_POLICY_ID"),
|
||||
returnPolicyId=require_env("EBAY_RETURN_POLICY_ID"),
|
||||
),
|
||||
pricingSummary=PricingSummary(
|
||||
price=Amount(
|
||||
currency=os.environ.get("EBAY_CURRENCY", "USD"),
|
||||
value=require_env("EBAY_PRICE"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = build_client()
|
||||
sku = require_env("EBAY_SKU")
|
||||
content_language = os.environ.get("EBAY_CONTENT_LANGUAGE", "en-US")
|
||||
|
||||
item_result = client.inventory.create_or_replace_inventory_item(
|
||||
sku,
|
||||
build_inventory_item(),
|
||||
content_language=content_language,
|
||||
)
|
||||
print("inventory_item_warnings:", item_result.warnings)
|
||||
|
||||
offer = client.inventory.create_offer(
|
||||
build_offer_payload(sku),
|
||||
content_language=content_language,
|
||||
)
|
||||
print("offer_id:", offer.offerId)
|
||||
|
||||
if not offer.offerId:
|
||||
raise RuntimeError("create_offer did not return an offerId")
|
||||
|
||||
published = client.inventory.publish_offer(offer.offerId)
|
||||
print("listing_id:", published.listingId)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue