qls-westcon-comstor
Python SDK for the Westcon-Comstor AIM (API Integration Management) APIs. Sync and async clients, typed request/response models (pydantic v2), OAuth2 handling and a consistent error hierarchy.
Status: Phase 1 — authentication + the 12 core transactional APIs are implemented and tested. PartnerView is reachable today via the generic
client.request(...)escape hatch and will get typed wrappers next (Phase 2, pending its spec).
Install
pip install qls-westcon-comstor # from PyPI
# or, from a checkout of this repo:
# pip install -e .
The distribution is qls-westcon-comstor; the import package is westcon_comstor.
Requires Python 3.10+. Depends on httpx and pydantic>=2.
Authentication
All transactional APIs use the OAuth 2.0 client-credentials flow against Azure AD (v1 token endpoint) and an APIM subscription key. Westcon-Comstor issues these values by email when your partner account is provisioned:
| Value | Where it goes |
|---|---|
client_id |
token request body |
client_secret |
token request body |
resource |
token request body (Azure AD resource) |
subscription_key |
Ocp-Apim-Subscription-Key header |
partner_key |
request body (partnerKey) |
The SDK fetches, caches and refreshes the bearer token for you (including a one-shot
refresh on a 401).
Configuration
Pass values explicitly, or set environment variables and let the client read them:
WESTCON_CLIENT_ID, WESTCON_CLIENT_SECRET, WESTCON_RESOURCE,
WESTCON_SUBSCRIPTION_KEY, WESTCON_PARTNER_KEY,
WESTCON_TENANT_ID (optional), WESTCON_GATEWAY_BASE_URL (optional)
WESTCON_TENANT_ID defaults to Westcon-Comstor's Azure AD tenant (the OAuth
authority in the token-endpoint URL Westcon issues you); set it only to override.
Default gateway: https://api.westconcomstor.com (prod), or uat per WESTCON_ENV below.
Environments. Keys are per-environment. Set WESTCON_ENV=prod (default) or
WESTCON_ENV=uat and the gateway is chosen for you (prod →
https://api.westconcomstor.com, uat → https://westconapiuat.azure-api.net). Keys
issued from the UAT portal (westconapiuat.developer.azure-api.net) only work against
UAT; using them on prod returns 401 invalid subscription key.
To keep both environments in one .env, set per-environment variables
(WESTCON_UAT_* and WESTCON_PROD_*) and flip WESTCON_ENV — the environment-specific
value wins over the generic WESTCON_*. See .env.example. In code,
Config(..., environment="uat") does the same.
Quick start (sync)
from westcon_comstor import WestconComstorClient
with WestconComstorClient(
client_id="...", client_secret="...", resource="...",
subscription_key="...", partner_key="...",
) as client:
# Product availability
products = client.products.availability(
country_code="AU",
products=[{"product_number": "C1111-4P"}],
)
for p in products:
for loc in p.storage_locations:
print(p.product_number, loc.plant, loc.available_quantity)
# Pricing
prices = client.products.pricing(
country_code="DE", currency="EUR",
products=[{"product_number": "ASG-WSWS-10-CS"}],
)
# Open invoices / orders
invoices = client.invoices.list(start_date="20250101", end_date="20250201")
orders = client.orders.open_order_list(start_date="20250101")
# Order status
lines = client.orders.status(["600014", "600019"])
# Quote
quote = client.quotes.get(quote_id="08XX58X2", reseller_id="0004704", version="0")
# Shipment tracking
track = client.shipping.carrier_tracking(
order_number="201201", carrier="DHL", tracking_number="JD0146...",
)
detail = client.shipping.shipment_detail(order_number="59XXXX", tracking_data="Y")
# Accounts (URUP)
acct = client.accounts.detail(customer="0011223")
hits = client.accounts.search(name="Technology Company", country="NL")
Quick start (async)
from westcon_comstor import AsyncWestconComstorClient
async with AsyncWestconComstorClient() as client: # reads WESTCON_* env vars
prices = await client.products.pricing(
country_code="DE", currency="EUR",
products=[{"product_number": "ASG-WSWS-10-CS"}],
)
API map
| Namespace | Method | Endpoint |
|---|---|---|
client.invoices |
list() |
POST /OpenInvoiceList/GetList |
client.orders |
open_order_list() |
POST /OpenOrders/GetList |
client.orders |
status() |
POST /Orders/orderStatus |
client.orders |
status_value() |
POST /orderstatusvalue/getorderstatusvalue |
client.orders |
invoiced_lines() |
derived (Order Status, INV lines) |
client.orders |
invoices() |
derived (INV lines grouped by invoice) |
client.products |
availability() |
POST /api/products/availability |
client.products |
pricing() |
POST /Pricing/RetrievePrice |
client.quotes |
get() |
POST /QuoteDetail/retrieve |
client.shipping |
carrier_tracking() |
POST /CarrierTracking/GetTrackingInfo |
client.shipping |
shipment_detail() |
POST /shipping/ShipmentDetail |
client.accounts |
detail() |
POST /accountdetail/getaccountdetail |
client.accounts |
search() |
POST /accountsearch/getaccounts |
client.aem |
order_now() |
POST /aem-secure-content/order_now |
client.fx |
implied_rate(), rates() |
derived (Pricing API) |
client.request() |
generic escape hatch (PartnerView) | any |
Derived exchange rates (client.fx)
Comstor has no exchange-rate endpoint, but its pricing engine converts a product's base
price into whatever currency you request. client.fx prices the same product(s) in two
currencies and divides the returned list prices to get the effective rate Comstor uses
(margin included). Pass several products to verify the rate is consistent:
rate = client.fx.implied_rate(base="USD", quote="SEK", products="SW-FWSWS-ASG-S500")
print(rate.rate) # median implied USD->SEK
# multiple base currencies against one target (SEK prices fetched once):
for r in client.fx.rates(bases=["EUR", "USD"], quote="SEK",
products=["SKU1", "SKU2", "SKU3"], country="SE"):
print(r.base_currency, r.rate, "spread", r.spread, "n", r.sample_count)
for s in r.samples: # per-product cross-check
print(" ", s.product_number, s.base_price, "->", s.quote_price, s.rate)
ImpliedRate carries rate (median), mean, minimum, maximum, spread,
sample_count, per-product samples, and skipped (products missing a price in one
currency). Uses listPrice by default (price_field="customer" to switch). It's a
snapshot — re-run to track changes. See examples/fx_rates.py.
Errors
All exceptions derive from westcon_comstor.WestconComstorError. HTTP failures raise an
APIStatusError subclass (BadRequestError, UnauthorizedError, ForbiddenError,
NotFoundError, UnprocessableEntityError, RateLimitError, ServerError) carrying
status_code, parsed body, and the APIM request_id (quote it in Westcon support
tickets). Token failures raise AuthenticationError; network/timeout failures raise
APIConnectionError / APITimeoutError.
Logging
The SDK logs to the westcon_comstor logger and is silent until your app adds a handler:
import logging
logging.getLogger("westcon_comstor").setLevel(logging.DEBUG) # DEBUG: path, timing, request_id
# WARNING level shows retries only.
It logs request method + URL, response status/latency, the APIM request_id, and retries.
It never logs credentials, tokens, headers, or request/response bodies (so no partner
key, no customer PII).
Development
pip install -e ".[dev]"
pytest
Roadmap
- Phase 2 — PartnerView (50+ ops: Commerce Webservices v2 — cart, onboarding,
finance, master data; REST path/query params). Callable today via
client.request(...).
Metadata
Release files for qls-westcon-comstor 26.9.28.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| qls_westcon_comstor-26.9.28.2.tar.gz | 60.1 kB | Without provenance | Details for qls_westcon_comstor-26.9.28.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| qls_westcon_comstor-26.9.28.2-py3-none-any.whl | Python 3 | none | any | Details for qls_westcon_comstor-26.9.28.2-py3-none-any.whl |
Total release size: 104.7 kB
Release files / qls_westcon_comstor-26.9.28.2.tar.gz
| Download URL | qls_westcon_comstor-26.9.28.2.tar.gz |
|---|---|
| Size | 60.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8b95f81fcc96b91b82c91367eeef762979a35001ada94c04c1b0d8c568faad82
|
|
BLAKE2b-256 checksum How to use checksums |
acbfe3fd915b0ac0c07d8447d53748c48d99685506756d8658bd6ec281bbb7d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Release files / qls_westcon_comstor-26.9.28.2-py3-none-any.whl
| Download URL | qls_westcon_comstor-26.9.28.2-py3-none-any.whl |
|---|---|
| Size | 44.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0afbc645fa0eb747088184901f089c53910582580658fe19af3f7a2c922faeb5
|
|
BLAKE2b-256 checksum How to use checksums |
b6ffab3eb6ebe0ddeca340f0934ed1b8ec529c218fbcacc345d5b57f550cdbd4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Security for qls-westcon-comstor 26.9.28.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.