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