Add examples for feed task management and inventory publishing workflows

This commit is contained in:
claudi 2026-04-07 11:31:21 +02:00
parent 2c6bd35ebb
commit 904f4e487e
3 changed files with 363 additions and 6 deletions

View file

@ -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:

View 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()

View 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()