Add inventory management methods and tests for offers and locations in InventoryClient

This commit is contained in:
claudi 2026-04-07 10:59:48 +02:00
parent 9592f38902
commit 1f06ec9e44
2 changed files with 461 additions and 1 deletions

View file

@ -2,10 +2,26 @@ from __future__ import annotations
from ebay_client.core.http.transport import ApiTransport
from ebay_client.generated.inventory.models import (
BaseResponse,
EbayOfferDetailsWithId,
EbayOfferDetailsWithKeys,
FeesSummaryResponse,
InventoryItem,
InventoryItemGroup,
InventoryLocation,
InventoryLocationFull,
InventoryLocationResponse,
InventoryItemWithSkuLocaleGroupid,
InventoryItems,
LocationResponse,
OfferKeysWithId,
OfferResponse,
OfferResponseWithListingId,
Offers,
PublishByInventoryItemGroupRequest,
PublishResponse,
WithdrawByInventoryItemGroupRequest,
WithdrawResponse,
)
INVENTORY_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory"
@ -17,6 +33,12 @@ class InventoryClient:
def __init__(self, transport: ApiTransport) -> None:
self.transport = transport
def _write_headers(self, *, content_language: str | None = None) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
if content_language is not None:
headers["Content-Language"] = content_language
return headers
def get_inventory_item(self, sku: str) -> InventoryItemWithSkuLocaleGroupid:
return self.transport.request_model(
InventoryItemWithSkuLocaleGroupid,
@ -25,6 +47,29 @@ class InventoryClient:
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
)
def create_or_replace_inventory_item(
self,
sku: str,
payload: InventoryItem,
*,
content_language: str,
) -> BaseResponse:
return self.transport.request_model(
BaseResponse,
"PUT",
f"/sell/inventory/v1/inventory_item/{sku}",
scopes=[INVENTORY_SCOPE],
headers=self._write_headers(content_language=content_language),
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def delete_inventory_item(self, sku: str) -> None:
self.transport.request_json(
"DELETE",
f"/sell/inventory/v1/inventory_item/{sku}",
scopes=[INVENTORY_SCOPE],
)
def get_inventory_items(self, *, limit: int | None = None, offset: int | None = None) -> InventoryItems:
return self.transport.request_model(
InventoryItems,
@ -34,6 +79,37 @@ class InventoryClient:
params={"limit": limit, "offset": offset},
)
def get_inventory_item_group(self, inventory_item_group_key: str) -> InventoryItemGroup:
return self.transport.request_model(
InventoryItemGroup,
"GET",
f"/sell/inventory/v1/inventory_item_group/{inventory_item_group_key}",
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
)
def create_or_replace_inventory_item_group(
self,
inventory_item_group_key: str,
payload: InventoryItemGroup,
*,
content_language: str,
) -> BaseResponse:
return self.transport.request_model(
BaseResponse,
"PUT",
f"/sell/inventory/v1/inventory_item_group/{inventory_item_group_key}",
scopes=[INVENTORY_SCOPE],
headers=self._write_headers(content_language=content_language),
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def delete_inventory_item_group(self, inventory_item_group_key: str) -> None:
self.transport.request_json(
"DELETE",
f"/sell/inventory/v1/inventory_item_group/{inventory_item_group_key}",
scopes=[INVENTORY_SCOPE],
)
def get_offer(self, offer_id: str) -> OfferResponseWithListingId:
return self.transport.request_model(
OfferResponseWithListingId,
@ -42,6 +118,44 @@ class InventoryClient:
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
)
def create_offer(
self,
payload: EbayOfferDetailsWithKeys,
*,
content_language: str,
) -> OfferResponse:
return self.transport.request_model(
OfferResponse,
"POST",
"/sell/inventory/v1/offer",
scopes=[INVENTORY_SCOPE],
headers=self._write_headers(content_language=content_language),
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def update_offer(
self,
offer_id: str,
payload: EbayOfferDetailsWithId,
*,
content_language: str,
) -> OfferResponse:
return self.transport.request_model(
OfferResponse,
"PUT",
f"/sell/inventory/v1/offer/{offer_id}",
scopes=[INVENTORY_SCOPE],
headers=self._write_headers(content_language=content_language),
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def delete_offer(self, offer_id: str) -> None:
self.transport.request_json(
"DELETE",
f"/sell/inventory/v1/offer/{offer_id}",
scopes=[INVENTORY_SCOPE],
)
def get_offers(self, *, limit: int | None = None, offset: int | None = None, sku: str | None = None) -> Offers:
return self.transport.request_model(
Offers,
@ -50,3 +164,123 @@ class InventoryClient:
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
params={"limit": limit, "offset": offset, "sku": sku},
)
def get_listing_fees(self, payload: OfferKeysWithId | None = None) -> FeesSummaryResponse:
return self.transport.request_model(
FeesSummaryResponse,
"POST",
"/sell/inventory/v1/offer/get_listing_fees",
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
headers={"Content-Type": "application/json"},
json_body=None if payload is None else payload.model_dump(by_alias=True, exclude_none=True),
)
def publish_offer(self, offer_id: str) -> PublishResponse:
return self.transport.request_model(
PublishResponse,
"POST",
f"/sell/inventory/v1/offer/{offer_id}/publish",
scopes=[INVENTORY_SCOPE],
)
def publish_offer_by_inventory_item_group(
self,
payload: PublishByInventoryItemGroupRequest,
) -> PublishResponse:
return self.transport.request_model(
PublishResponse,
"POST",
"/sell/inventory/v1/offer/publish_by_inventory_item_group",
scopes=[INVENTORY_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def withdraw_offer(self, offer_id: str) -> WithdrawResponse:
return self.transport.request_model(
WithdrawResponse,
"POST",
f"/sell/inventory/v1/offer/{offer_id}/withdraw",
scopes=[INVENTORY_SCOPE],
)
def withdraw_offer_by_inventory_item_group(
self,
payload: WithdrawByInventoryItemGroupRequest,
) -> None:
self.transport.request_json(
"POST",
"/sell/inventory/v1/offer/withdraw_by_inventory_item_group",
scopes=[INVENTORY_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def get_inventory_location(self, merchant_location_key: str) -> InventoryLocationResponse:
return self.transport.request_model(
InventoryLocationResponse,
"GET",
f"/sell/inventory/v1/location/{merchant_location_key}",
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
)
def create_inventory_location(
self,
merchant_location_key: str,
payload: InventoryLocationFull,
) -> None:
self.transport.request_json(
"POST",
f"/sell/inventory/v1/location/{merchant_location_key}",
scopes=[INVENTORY_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)
def delete_inventory_location(self, merchant_location_key: str) -> None:
self.transport.request_json(
"DELETE",
f"/sell/inventory/v1/location/{merchant_location_key}",
scopes=[INVENTORY_SCOPE],
)
def disable_inventory_location(self, merchant_location_key: str) -> None:
self.transport.request_json(
"POST",
f"/sell/inventory/v1/location/{merchant_location_key}/disable",
scopes=[INVENTORY_SCOPE],
)
def enable_inventory_location(self, merchant_location_key: str) -> None:
self.transport.request_json(
"POST",
f"/sell/inventory/v1/location/{merchant_location_key}/enable",
scopes=[INVENTORY_SCOPE],
)
def get_inventory_locations(
self,
*,
limit: int | None = None,
offset: int | None = None,
) -> LocationResponse:
return self.transport.request_model(
LocationResponse,
"GET",
"/sell/inventory/v1/location",
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
params={"limit": limit, "offset": offset},
)
def update_inventory_location(
self,
merchant_location_key: str,
payload: InventoryLocation,
) -> None:
self.transport.request_json(
"POST",
f"/sell/inventory/v1/location/{merchant_location_key}/update_location_details",
scopes=[INVENTORY_SCOPE],
headers={"Content-Type": "application/json"},
json_body=payload.model_dump(by_alias=True, exclude_none=True),
)

View file

@ -27,7 +27,28 @@ from ebay_client.generated.feed.models import (
UserScheduleResponse,
)
from ebay_client.generated.fulfillment.models import Order
from ebay_client.generated.inventory.models import InventoryItemWithSkuLocaleGroupid
from ebay_client.generated.inventory.models import (
Address,
BaseResponse,
EbayOfferDetailsWithId,
EbayOfferDetailsWithKeys,
FeesSummaryResponse,
InventoryItem,
InventoryItemGroup,
InventoryItemWithSkuLocaleGroupid,
InventoryLocation,
InventoryLocationFull,
InventoryLocationResponse,
LocationDetails,
LocationResponse,
OfferKeyWithId,
OfferKeysWithId,
OfferResponse,
PublishByInventoryItemGroupRequest,
PublishResponse,
WithdrawByInventoryItemGroupRequest,
WithdrawResponse,
)
from ebay_client.generated.media.models import (
CreateDocumentFromUrlRequest,
CreateDocumentRequest,
@ -277,6 +298,211 @@ def test_inventory_wrapper_returns_inventory_item_model(httpx_mock: HTTPXMock) -
assert result.sku == "SKU-1"
def test_inventory_wrapper_supports_core_item_offer_and_group_workflows(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="PUT",
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-1",
json={"warnings": []},
)
httpx_mock.add_response(
method="DELETE",
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-1",
status_code=204,
)
httpx_mock.add_response(
method="PUT",
url="https://api.ebay.com/sell/inventory/v1/inventory_item_group/GROUP-1",
json={"warnings": []},
)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/inventory/v1/inventory_item_group/GROUP-1",
json={"inventoryItemGroupKey": "GROUP-1", "variantSKUs": ["SKU-1"]},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/offer",
json={"offerId": "OFFER-1"},
status_code=201,
)
httpx_mock.add_response(
method="PUT",
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1",
json={"warnings": []},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1/publish",
json={"offerId": "OFFER-1", "listingId": "ITEM-1"},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1/withdraw",
json={"offerId": "OFFER-1", "listingId": "ITEM-1"},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/offer/publish_by_inventory_item_group",
json={"listingId": "ITEM-2"},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/offer/withdraw_by_inventory_item_group",
status_code=204,
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/offer/get_listing_fees",
json={"feeSummaries": []},
)
httpx_mock.add_response(
method="DELETE",
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1",
status_code=204,
)
httpx_mock.add_response(
method="DELETE",
url="https://api.ebay.com/sell/inventory/v1/inventory_item_group/GROUP-1",
status_code=204,
)
client = InventoryClient(build_transport())
item_result = client.create_or_replace_inventory_item(
"SKU-1",
InventoryItem(condition="NEW"),
content_language="en-US",
)
client.delete_inventory_item("SKU-1")
group_result = client.create_or_replace_inventory_item_group(
"GROUP-1",
InventoryItemGroup(variantSKUs=["SKU-1"]),
content_language="en-US",
)
group = client.get_inventory_item_group("GROUP-1")
offer = client.create_offer(
EbayOfferDetailsWithKeys(sku="SKU-1", marketplaceId="EBAY_US", format="FIXED_PRICE"),
content_language="en-US",
)
updated_offer = client.update_offer(
"OFFER-1",
EbayOfferDetailsWithId(availableQuantity=2),
content_language="en-US",
)
published_offer = client.publish_offer("OFFER-1")
withdrawn_offer = client.withdraw_offer("OFFER-1")
group_publish = client.publish_offer_by_inventory_item_group(
PublishByInventoryItemGroupRequest(inventoryItemGroupKey="GROUP-1", marketplaceId="EBAY_US")
)
client.withdraw_offer_by_inventory_item_group(
WithdrawByInventoryItemGroupRequest(inventoryItemGroupKey="GROUP-1", marketplaceId="EBAY_US")
)
fees = client.get_listing_fees(OfferKeysWithId(offers=[OfferKeyWithId(offerId="OFFER-1")]))
client.delete_offer("OFFER-1")
client.delete_inventory_item_group("GROUP-1")
assert isinstance(item_result, BaseResponse)
assert isinstance(group_result, BaseResponse)
assert isinstance(group, InventoryItemGroup)
assert group.inventoryItemGroupKey == "GROUP-1"
assert isinstance(offer, OfferResponse)
assert offer.offerId == "OFFER-1"
assert isinstance(updated_offer, OfferResponse)
assert isinstance(published_offer, PublishResponse)
assert published_offer.listingId == "ITEM-1"
assert isinstance(withdrawn_offer, WithdrawResponse)
assert withdrawn_offer.listingId == "ITEM-1"
assert isinstance(group_publish, PublishResponse)
assert group_publish.listingId == "ITEM-2"
assert isinstance(fees, FeesSummaryResponse)
item_request = httpx_mock.get_requests()[0]
assert item_request.headers["Content-Language"] == "en-US"
item_body = json.loads(item_request.content.decode("utf-8"))
assert item_body["condition"] == "NEW"
offer_request = httpx_mock.get_requests()[4]
assert offer_request.headers["Content-Language"] == "en-US"
offer_body = json.loads(offer_request.content.decode("utf-8"))
assert offer_body["sku"] == "SKU-1"
assert offer_body["marketplaceId"] == "EBAY_US"
update_request = httpx_mock.get_requests()[5]
update_body = json.loads(update_request.content.decode("utf-8"))
assert update_body["availableQuantity"] == 2
group_publish_request = httpx_mock.get_requests()[8]
assert group_publish_request.headers["Content-Type"] == "application/json"
group_publish_body = json.loads(group_publish_request.content.decode("utf-8"))
assert group_publish_body["inventoryItemGroupKey"] == "GROUP-1"
fees_request = httpx_mock.get_requests()[10]
fees_body = json.loads(fees_request.content.decode("utf-8"))
assert fees_body["offers"] == [{"offerId": "OFFER-1"}]
def test_inventory_wrapper_supports_location_management(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1",
status_code=204,
)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1",
json={"merchantLocationKey": "LOC-1", "name": "Main warehouse"},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1/update_location_details",
status_code=204,
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1/disable",
json={},
)
httpx_mock.add_response(
method="POST",
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1/enable",
json={},
)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/inventory/v1/location?limit=25&offset=0",
json={"locations": [{"merchantLocationKey": "LOC-1"}], "total": 1},
)
httpx_mock.add_response(
method="DELETE",
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1",
status_code=204,
)
client = InventoryClient(build_transport())
client.create_inventory_location(
"LOC-1",
InventoryLocationFull(location=LocationDetails(address=Address(country="US", postalCode="10001"))),
)
location = client.get_inventory_location("LOC-1")
client.update_inventory_location("LOC-1", InventoryLocation(name="Main warehouse"))
client.disable_inventory_location("LOC-1")
client.enable_inventory_location("LOC-1")
locations = client.get_inventory_locations(limit=25, offset=0)
client.delete_inventory_location("LOC-1")
assert isinstance(location, InventoryLocationResponse)
assert location.merchantLocationKey == "LOC-1"
assert isinstance(locations, LocationResponse)
assert locations.total == 1
create_request = httpx_mock.get_requests()[0]
create_body = json.loads(create_request.content.decode("utf-8"))
assert create_body["location"]["address"]["country"] == "US"
update_request = httpx_mock.get_requests()[2]
update_body = json.loads(update_request.content.decode("utf-8"))
assert update_body["name"] == "Main warehouse"
def test_fulfillment_wrapper_returns_order_model(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="GET",