Skip to content

Commit 531c13a

Browse files
authored
Add support for 2025 GMC EVs (#26)
* Bump package versions * Add support for GMC EVs * Ignore E231 rule * Add support for GMC EVs
1 parent ea0edf6 commit 531c13a

File tree

7 files changed

+188
-20
lines changed

7 files changed

+188
-20
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
# https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length
66
max-line-length = 88
77
select = C,E,F,W,B,B950
8-
extend-ignore = E203,E501,W503,B008
8+
extend-ignore = E203,E231,E501,W503,B008

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
fastapi==0.115.6
1+
fastapi==0.115.12
22
google-cloud-error-reporting==1.11.1
33
httpx[http2]==0.28.1
4-
uvicorn[standard]==0.32.1
4+
uvicorn[standard]==0.34.2

requirements_dev.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
-r requirements.txt
33

44
# Tests and linting
5-
Faker==33.1.0
6-
flake8==7.1.1
7-
flake8-bugbear==24.10.31
5+
Faker==37.1.0
6+
flake8==7.2.0
7+
flake8-bugbear==24.12.12
88
ipykernel==6.29.5
9-
pre_commit==4.0.1
10-
pytest==8.3.4
11-
vcrpy==6.0.2
9+
pre_commit==4.2.0
10+
pytest==8.3.5
11+
vcrpy==7.0.0

src/libs/common_query_params.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ class CommonInventoryQueryParams:
3636
"^i5$", # BMW i5
3737
"^i7$", # BMW i7
3838
"^9$", # BMW ix
39+
r"^sierra(%20|\+|\s|\-)ev", # GMC Sierra EV
40+
r"^hummer(%20|\+|\s|\-)ev(%20|\+|\s|\-)pickup", # GMC HUMMER EV Pickup
41+
r"^hummer(%20|\+|\s|\-)ev(%20|\+|\s|\-)suv", # GMC HUMMER EV SUV
3942
]
4043

4144
def __init__(

src/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
chevrolet,
1010
ford,
1111
genesis,
12+
gmc,
1213
helpers,
1314
hyundai,
1415
kia,
@@ -23,6 +24,7 @@
2324
app.include_router(chevrolet.router)
2425
app.include_router(ford.router)
2526
app.include_router(genesis.router)
27+
app.include_router(gmc.router)
2628
app.include_router(hyundai.router)
2729
app.include_router(kia.router)
2830
app.include_router(volkswagen.router)

src/routers/ford.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ async def main(
5151
"zipcode": zip_code,
5252
}
5353

54-
# Ford apparently does not support radius searches > 500 miles. For now, returning
55-
# an error message to users who attempt a search radius > 500 miles.
56-
# TODO: Deal with this in the UI, with better info messaging
57-
if int(radius) > 500:
58-
return error_response(
59-
error_message="Retry your request with a radius between 1 and 500 miles.",
60-
error_data="",
61-
status_code=400,
62-
)
63-
6454
dealers_uri = "/aemservices/cache/inventory/dealer/dealers"
6555
inventory_uri = "/aemservices/cache/inventory/dealer-lot"
6656

@@ -190,7 +180,7 @@ async def main(
190180
inv["rdata"] = {"vehicles": vehicles, "dealers": dealers}
191181

192182
end = time.perf_counter()
193-
print(f"\n\n-----\nTime taken for Ford API transaction: {end-start} sec")
183+
print(f"\n\n-----\nTime taken for Ford API transaction: {end - start} sec")
194184

195185
await http.close()
196186
return send_response(response_data=inv, cache_control_age=3600)

src/routers/gmc.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
gmc_base_url = "https://cws.gm.com"
26+
page_size = 96
27+
generic_error_message = "An error occurred obtaining GMC inventory results."
28+
29+
30+
@router.get("/inventory/gmc")
31+
async def get_gmc_inventory(
32+
req: Request, req_params: CommonInventoryQueryParams = Depends()
33+
) -> dict:
34+
params = {
35+
"conditions": "New",
36+
"makes": "GMC",
37+
"locale": "en_US",
38+
"models": req_params.model,
39+
"years": req_params.year,
40+
"radius": req_params.radius,
41+
"postalCode": req_params.zip,
42+
"pageSize": page_size,
43+
"sortby": "bestMatch:desc,distance:asc,netPrice:asc",
44+
"includeNearMatches": "true",
45+
"requesterType": "TIER_1_VSR",
46+
}
47+
headers = {
48+
"User-Agent": req.headers.get("User-Agent"),
49+
"Referer": "https://www.gmc.com/",
50+
}
51+
52+
inventory_uri = "/vs-cws/vehshop/v2/vehicles"
53+
54+
# Setup the HTTPX client to be used for the many API calls throughout this router
55+
http = AsyncHTTPClient(base_url=gmc_base_url, timeout_value=30.0, verify=verify_ssl)
56+
57+
# Retrieve the initial batch of {page_size} vehicles
58+
i = await http.get(uri=inventory_uri, headers=headers, params=params)
59+
try:
60+
inventory = i.json()
61+
except ValueError:
62+
return error_response(error_message=generic_error_message)
63+
64+
# Ensure the response back from the API has some status, indicating a successful
65+
# API call
66+
try:
67+
inventory["resultsCount"]
68+
except KeyError:
69+
return error_response(
70+
error_message=generic_error_message,
71+
error_data=inventory,
72+
status_code=500,
73+
)
74+
75+
inventory_result_count = inventory.get("resultsCount")
76+
# We have only one page of results, so just returning the JSON response back to the
77+
# frontend.
78+
if inventory_result_count <= page_size:
79+
if not inventory.get("error"):
80+
return send_response(response_data=inventory)
81+
else:
82+
# The GMC inventory API pages {page_size} vehicles at a time. Making N number of
83+
# API requests, incremented by step.
84+
begin_index = page_size
85+
end_index = 0
86+
step = page_size
87+
88+
urls_to_fetch = []
89+
90+
for i in range(begin_index, inventory_result_count, step):
91+
begin_index = i
92+
93+
# Ensure we don't request more than the inventory_result_count of pages
94+
# returned for this inventory request
95+
if i + step < inventory_result_count:
96+
end_index = i + step
97+
else:
98+
end_index = inventory_result_count
99+
100+
# Adding beginIndex and endIndex to the query params used to make subsequent
101+
# API requests
102+
remainder_inventory_params = {
103+
**params,
104+
"pageSize": end_index,
105+
}
106+
107+
# Create a list of requests which will be passed to httpx
108+
urls_to_fetch.append(
109+
[
110+
inventory_uri,
111+
headers,
112+
remainder_inventory_params,
113+
]
114+
)
115+
116+
remainder = await http.get(uri=urls_to_fetch)
117+
118+
# If we only have the initial API call and one additional API call, the response
119+
# back from the http helper library is a httpx.Response object. If we have multiple
120+
# additional API calls, the response back is a list. Catching this situation and
121+
# throwing that single httpx.Response object into a list for further processing.
122+
if type(remainder) is not list:
123+
remainder = [remainder]
124+
125+
# Loop through the inventory results list
126+
for api_result in remainder:
127+
# When issuing concurrent API requests, some may come back with non-200
128+
# responses (e.g. 500) and thus no JSON response data. Catching that condition
129+
# and adding an item to the dict which is returned to the front end.
130+
try:
131+
result = api_result.json()
132+
inventory["vehicles"].append(result["vehicles"])
133+
except AttributeError:
134+
i["apiErrorResponse"] = True
135+
136+
await http.close()
137+
return send_response(response_data=inventory, cache_control_age=3600)
138+
139+
140+
@router.get("/vin/gmc")
141+
async def get_gmc_vin_detail(req: Request) -> dict:
142+
# Make a call to the GMC API
143+
async with AsyncHTTPClient(
144+
base_url=gmc_base_url, timeout_value=30.0, verify=verify_ssl
145+
) as http:
146+
params = {
147+
"vin:": req.query_params.get("vin"),
148+
"postalCode": req.query_params.get("postalCode"),
149+
"customerType": "GC",
150+
"requesterType": "TIER_1",
151+
"locale": "en_US",
152+
}
153+
headers = {"User-Agent": req.headers.get("User-Agent"), "referer": gmc_base_url}
154+
155+
v = await http.get(
156+
uri="/vs-cws/vehshop/v2/vehicle",
157+
headers=headers,
158+
params=params,
159+
)
160+
161+
try:
162+
vin_data = v.json()
163+
except AttributeError:
164+
return error_response(error_message=v, status_code=504)
165+
else:
166+
if req.query_params.get("vin") in vin_data["vin"]:
167+
return send_response(response_data=vin_data)
168+
else:
169+
return error_response(
170+
error_message="An error occurred obtaining VIN detail for this vehicle.",
171+
error_data=vin_data.split(":")[0],
172+
status_code=400,
173+
)

0 commit comments

Comments
 (0)