165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
"""
|
||
Dell asset lookup service.
|
||
|
||
Two modes (chosen automatically):
|
||
|
||
1. **No credentials** – Returns a partial pre-fill (brand=Dell, OS default, service_tag).
|
||
Model and warranty must be filled in manually; a link to Dell's support page is provided.
|
||
Dell's public website is protected by Akamai and cannot be scraped reliably.
|
||
|
||
2. **TechDirect API** – Full data including model, warranty dates, serial number.
|
||
Register free at https://tdm.dell.com → API Services → Create an API key pair.
|
||
Set DELL_CLIENT_ID and DELL_CLIENT_SECRET in your .env file.
|
||
"""
|
||
import logging
|
||
from datetime import datetime
|
||
|
||
import requests
|
||
from flask import current_app
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
_TOKEN_URL = "https://apigtwb2c.us.dell.com/auth/oauth/v2/token"
|
||
_ASSET_URL = "https://apigtwb2c.us.dell.com/PROD/sbil/eapi/v5/asset-entitlements"
|
||
_SUPPORT_PAGE = "https://www.dell.com/support/home/en-us/product-support/servicetag/{tag}/overview"
|
||
|
||
_TYPE_MAP = [
|
||
(["latitude", "inspiron", "xps", "vostro", "precision 5", "precision 7"], "Laptop"),
|
||
(["optiplex", "precision tower", "precision 3", "precision 9",
|
||
"optiplex micro", "optiplex small"], "Desktop"),
|
||
(["poweredge", "server"], "Server"),
|
||
(["wyse", "thin client"], "Other"),
|
||
(["monitor", "display", "screen", "s24", "s27", "p24", "u27"], "Monitor"),
|
||
]
|
||
|
||
|
||
def _detect_type(description: str) -> str:
|
||
desc = description.lower()
|
||
for keywords, asset_type in _TYPE_MAP:
|
||
if any(kw in desc for kw in keywords):
|
||
return asset_type
|
||
return "Laptop" # sensible default for Dell business hardware
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# Partial pre-fill (no credentials)
|
||
# ------------------------------------------------------------------
|
||
|
||
def _partial_prefill(tag: str) -> dict:
|
||
"""
|
||
Return a minimal pre-fill dict using only what we know without querying Dell.
|
||
Includes a link to Dell's support page so the user can look up the rest.
|
||
"""
|
||
return {
|
||
"service_tag": tag,
|
||
"serial_number": "",
|
||
"brand": "Dell",
|
||
"model": "",
|
||
"asset_type": "Laptop",
|
||
"operating_system": "Windows 11 Pro",
|
||
"warranty_expiry": "",
|
||
"purchase_date": "",
|
||
"source": "partial",
|
||
"support_url": _SUPPORT_PAGE.format(tag=tag),
|
||
}
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# Official TechDirect API (requires credentials)
|
||
# ------------------------------------------------------------------
|
||
|
||
def _get_token(client_id: str, client_secret: str) -> str:
|
||
resp = requests.post(
|
||
_TOKEN_URL,
|
||
data={
|
||
"grant_type": "client_credentials",
|
||
"client_id": client_id,
|
||
"client_secret": client_secret,
|
||
},
|
||
timeout=10,
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.json()["access_token"]
|
||
|
||
|
||
def _lookup_api(tag: str, client_id: str, client_secret: str) -> dict:
|
||
try:
|
||
token = _get_token(client_id, client_secret)
|
||
except Exception as exc:
|
||
raise RuntimeError(f"Failed to obtain Dell API token: {exc}") from exc
|
||
|
||
try:
|
||
resp = requests.get(
|
||
_ASSET_URL,
|
||
params={"servicetags": tag},
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
timeout=15,
|
||
)
|
||
resp.raise_for_status()
|
||
data = resp.json()
|
||
except Exception as exc:
|
||
raise RuntimeError(f"Dell API request failed: {exc}") from exc
|
||
|
||
if not data:
|
||
raise RuntimeError(f"No data returned for service tag '{tag}'.")
|
||
|
||
item = data[0] if isinstance(data, list) else data
|
||
system_desc = item.get("productLineDescription") or item.get("systemDescription") or ""
|
||
model = system_desc.strip()
|
||
serial_number = (item.get("serviceTag") or tag).upper()
|
||
|
||
warranty_expiry = None
|
||
for ent in (item.get("entitlements") or []):
|
||
end_str = ent.get("endDate") or ""
|
||
if end_str:
|
||
try:
|
||
dt = datetime.fromisoformat(end_str[:10])
|
||
if warranty_expiry is None or dt > warranty_expiry:
|
||
warranty_expiry = dt
|
||
except ValueError:
|
||
pass
|
||
|
||
ship_date = item.get("shipDate") or ""
|
||
purchase_date = None
|
||
if ship_date:
|
||
try:
|
||
purchase_date = datetime.fromisoformat(ship_date[:10])
|
||
except ValueError:
|
||
pass
|
||
|
||
return {
|
||
"service_tag": tag,
|
||
"serial_number": serial_number,
|
||
"brand": "Dell",
|
||
"model": model,
|
||
"asset_type": _detect_type(f"{system_desc} {item.get('productFamily', '')}"),
|
||
"operating_system": "Windows 11 Pro",
|
||
"warranty_expiry": warranty_expiry.strftime("%Y-%m-%d") if warranty_expiry else "",
|
||
"purchase_date": purchase_date.strftime("%Y-%m-%d") if purchase_date else "",
|
||
"source": "techdirect_api",
|
||
"support_url": _SUPPORT_PAGE.format(tag=tag),
|
||
}
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# Public entry point
|
||
# ------------------------------------------------------------------
|
||
|
||
def lookup_service_tag(service_tag: str) -> dict:
|
||
"""
|
||
Look up a Dell service tag.
|
||
|
||
Uses TechDirect API when DELL_CLIENT_ID + DELL_CLIENT_SECRET are configured;
|
||
otherwise returns a partial pre-fill with a link to Dell's support page.
|
||
"""
|
||
tag = service_tag.strip().upper()
|
||
client_id = current_app.config.get("DELL_CLIENT_ID", "")
|
||
client_secret = current_app.config.get("DELL_CLIENT_SECRET", "")
|
||
|
||
if client_id and client_secret:
|
||
log.debug("Dell lookup via TechDirect API for %s", tag)
|
||
return _lookup_api(tag, client_id, client_secret)
|
||
|
||
log.debug("Dell lookup returning partial pre-fill for %s (no API credentials)", tag)
|
||
return _partial_prefill(tag)
|