|
| 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