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. 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 ### Notification: manage webhook subscriptions
Typical flow: Typical flow:
@ -143,7 +157,7 @@ Recommended server flow:
3. verify the POST body with `WebhookSignatureValidator` 3. verify the POST body with `WebhookSignatureValidator`
4. dispatch the parsed `WebhookEventEnvelope` 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 ### Inventory: read listings and inventory records
@ -305,9 +319,157 @@ Helpers:
- `VideoWorkflowResult` - `VideoWorkflowResult`
- `DocumentWorkflowResult` - `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. 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. 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. - 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. - 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. - 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. - 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: Run tests:
@ -364,4 +526,4 @@ Generate just one API package:
& .\.venv\Scripts\python.exe .\scripts\generate_clients.py --api media & .\.venv\Scripts\python.exe .\scripts\generate_clients.py --api media
``` ```
The generated models are written to `ebay_client/generated/<api>/models.py`. The generated models are written to `ebay_client/generated/<api>/models.py`.

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