Skip to content

Commit c7ac41b

Browse files
authored
Add support for 2026 model year and Cadillac EVs (#28)
* Add support for Cadillac EVs * Add support for 2026 model year
1 parent ace8bd3 commit c7ac41b

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed

src/libs/common_query_params.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class CommonInventoryQueryParams:
3939
r"^sierra(%20|\+|\s|\-)ev", # GMC Sierra EV
4040
r"^hummer(%20|\+|\s|\-)ev(%20|\+|\s|\-)pickup", # GMC HUMMER EV Pickup
4141
r"^hummer(%20|\+|\s|\-)ev(%20|\+|\s|\-)suv", # GMC HUMMER EV SUV
42+
r"^escalade(%20|\+|\s|\-)iq", # Cadillac Escalade IQ
43+
"lyriq", # Cadillac Lyriq
44+
"optiq", # Cadillac Optiq
45+
"vistiq", # Cadillac Vistiq
4246
]
4347

4448
def __init__(

src/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from src.routers import (
77
audi,
88
bmw,
9+
cadillac,
910
chevrolet,
1011
ford,
1112
genesis,
@@ -21,6 +22,7 @@
2122

2223
app.include_router(bmw.router)
2324
app.include_router(audi.router)
25+
app.include_router(cadillac.router)
2426
app.include_router(chevrolet.router)
2527
app.include_router(ford.router)
2628
app.include_router(genesis.router)

src/routers/cadillac.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Copyright 2025 Ben Chapman
2+
#
3+
# This file is part of The EV Finder.
4+
#
5+
# The EV Finder is free software: you can redistribute it and/or modify it under the
6+
# terms of the GNU General Public License as published by the Free Software Foundation,
7+
# either version 3 of the License, or (at your option) any later version.
8+
#
9+
# The EV Finder is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10+
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11+
# See the GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License along with The EV Finder.
14+
# If not, see <https://www.gnu.org/licenses/>.
15+
16+
17+
from fastapi import APIRouter, Depends, Request
18+
19+
from src.libs.common_query_params import CommonInventoryQueryParams
20+
from src.libs.http import AsyncHTTPClient
21+
from src.libs.responses import error_response, send_response
22+
23+
router = APIRouter(prefix="/api")
24+
verify_ssl = True
25+
26+
cadillac_base_url = "https://www.cadillac.com/cadillac/shopping/api"
27+
inventory_uri = "/aec-cp-discovery-api/p/v1/vehicles/search"
28+
vin_uri = "/aec-cp-ims-apigateway/p/v1/vehicles/detail"
29+
page_size = 20
30+
31+
generic_error_message = "An error occurred obtaining Cadillac inventory results."
32+
33+
34+
@router.get("/inventory/cadillac")
35+
async def get_cadillac_inventory(
36+
req: Request, req_params: CommonInventoryQueryParams = Depends()
37+
) -> dict:
38+
zip_code = str(req_params.zip)
39+
year = str(req_params.year)
40+
model = req_params.model
41+
radius = req_params.radius
42+
43+
inventory_post_data = {
44+
"filters": {
45+
"vehicleCategory": {"values": ["EV"]},
46+
"year": {"values": [year]},
47+
"model": {"values": [model]},
48+
"geo": {"zipCode": zip_code, "radius": radius},
49+
},
50+
"sort": {"name": "distance", "order": "ASC"},
51+
"paymentTypes": ["CASH"],
52+
"pagination": {"size": page_size},
53+
}
54+
headers = {
55+
"User-Agent": req.headers.get("User-Agent"),
56+
"Referer": f"https://www.cadillac.com/shopping/inventory/search/{model}/{year}",
57+
"oemId": "GM",
58+
"programId": "CADILLAC",
59+
"dealerId": "0",
60+
"tenantId": "0",
61+
"client": "T1_VSR",
62+
}
63+
64+
# Setup the HTTPX client to be used for the many API calls throughout this router
65+
http = AsyncHTTPClient(
66+
base_url=cadillac_base_url, timeout_value=30.0, verify=verify_ssl
67+
)
68+
69+
# Some Cadillac vehicle detail is accessible through a separate API endpoint. Making
70+
# that call here, and will combine with the inventory results later.
71+
facets_post_data = {
72+
"filters": {
73+
"model": {"values": [model]},
74+
"geo": {"zipCode": zip_code, "radius": radius},
75+
}
76+
}
77+
f = await http.post(
78+
uri="/aec-cp-discovery-api/p/v1/vehicles/facets",
79+
headers=headers,
80+
post_data=facets_post_data,
81+
)
82+
try:
83+
facets = f.json()
84+
except ValueError:
85+
pass
86+
87+
# Retrieve the initial batch of vehicles
88+
i = await http.post(
89+
uri=inventory_uri, headers=headers, post_data=inventory_post_data
90+
)
91+
try:
92+
inventory = i.json()
93+
except ValueError:
94+
return error_response(error_message=generic_error_message)
95+
96+
# Ensure the response back from the API has some status, indicating a successful
97+
# API call
98+
try:
99+
inventory["status"]
100+
except KeyError:
101+
return error_response(
102+
error_message=generic_error_message,
103+
error_data=inventory,
104+
status_code=500,
105+
)
106+
107+
# The Cadillac API returns a 404 and JSON response with a inventory.notfound key if
108+
# no vehicles are found. Checking for that condition and returning an empty dict
109+
if (
110+
inventory.get("errorDetails")
111+
and inventory["errorDetails"]["key"] == "inventory.notFound"
112+
):
113+
return send_response(response_data={})
114+
115+
inventory_result_count = inventory.get("data").get("count")
116+
117+
# We have only one page of results, so just return the JSON response back to the
118+
# frontend.
119+
if inventory_result_count <= page_size:
120+
return send_response(response_data=inventory)
121+
else:
122+
# The Cadillac inventory API pages 20 vehicles at a time. Making N number of API
123+
# requests, incremented by step.
124+
begin_index = page_size
125+
step = page_size
126+
# nextPageToken is required to be sent with the API request to retrieve the next
127+
# page of vehicles
128+
next_page_token = inventory.get("data").get("pagination").get("nextPageToken")
129+
130+
for i in range(begin_index, inventory_result_count, step):
131+
begin_index = i
132+
133+
# Add the nextPageToken to subsequent requests
134+
remainder_inventory_post_data = {
135+
**inventory_post_data,
136+
"pagination": {
137+
"size": page_size,
138+
"nextPageToken": next_page_token,
139+
},
140+
}
141+
142+
# The Cadillac API uses nextPageToken to retrieve the next page of vehicles
143+
# so we have to loop through the
144+
remainder_i = await http.post(
145+
uri=inventory_uri,
146+
headers=headers,
147+
post_data=remainder_inventory_post_data,
148+
)
149+
150+
# Push this page of results into the inventory dict
151+
try:
152+
remainder = remainder_i.json()
153+
inventory["data"]["hits"] = (
154+
inventory["data"]["hits"] + remainder["data"]["hits"]
155+
)
156+
except AttributeError:
157+
i["apiErrorResponse"] = True
158+
159+
next_page_token = (
160+
remainder.get("data").get("pagination").get("nextPageToken")
161+
)
162+
163+
await http.close()
164+
165+
# Combine the facets data with the inventory data
166+
inventory["facets"] = facets
167+
return send_response(response_data=inventory, cache_control_age=3600)
168+
169+
170+
@router.get("/vin/cadillac")
171+
async def get_cadillac_vin_detail(req: Request) -> dict:
172+
# Make a call to the Cadillac API
173+
async with AsyncHTTPClient(
174+
base_url=cadillac_base_url, timeout_value=30.0, verify=verify_ssl
175+
) as http:
176+
vin_post_data = {
177+
"pricing": {
178+
"paymentTypes": ["CASH", "FINANCE", "LEASE"],
179+
"finance": {"downPayment": 3500},
180+
"lease": {"mileage": 10000, "downPayment": 3500},
181+
},
182+
"vin": req.query_params.get("vin"),
183+
}
184+
185+
headers = {
186+
"User-Agent": req.headers.get("User-Agent"),
187+
"Referer": f"{cadillac_base_url}/shopping/inventory/vehicle/\
188+
{req.query_params.get('model').upper()}/{req.query_params.get('year')}",
189+
"oemId": "GM",
190+
"programId": "CADILLAC",
191+
"dealerId": "0",
192+
"tenantId": "0",
193+
"client": "UI",
194+
}
195+
196+
v = await http.post(
197+
uri=vin_uri,
198+
headers=headers,
199+
post_data=vin_post_data,
200+
)
201+
202+
try:
203+
vin_data = v.json()
204+
except AttributeError:
205+
return error_response(error_message=v, status_code=504)
206+
else:
207+
if req.query_params.get("vin") in vin_data["data"]["id"]:
208+
return send_response(response_data=vin_data)
209+
else:
210+
return error_response(
211+
error_message="An error occurred obtaining VIN detail for this vehicle.",
212+
error_data=vin_data["errorDetails"]["errorCode"],
213+
status_code=400,
214+
)

0 commit comments

Comments
 (0)