Skip to content

Commit 4b33021

Browse files
authored
Adding support for the 2024 model year (#10)
* Adding support for 2024 model year * Adding httpstat.us helper
1 parent 04ffd35 commit 4b33021

File tree

7 files changed

+125
-21
lines changed

7 files changed

+125
-21
lines changed

src/libs/common_query_params.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(
3535
self,
3636
# https://facts.usps.com/42000-zip-codes/. Starting zip code is 00501
3737
zip: int = Query(ge=501, le=99950),
38-
year: int = Query(ge=2022, le=2023),
38+
year: int = Query(ge=2022, le=2024),
3939
radius: int = Query(gt=0, lt=1000),
4040
model: str = Query(regex="|".join(valid_models)),
4141
):

src/routers/bmw.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ async def get_bmw_inventory(
1818
max_page_size = 2000
1919

2020
zip_code = str(common_params.zip)
21+
year = str(common_params.year)
2122
model = common_params.model
2223
radius = str(common_params.radius)
2324

@@ -33,6 +34,8 @@ async def get_bmw_inventory(
3334
+ radius
3435
+ " excludeStopSale: false series: "
3536
+ f'"{model}"'
37+
+ f" minModelYear: {year}"
38+
+ f" maxModelYear: {year}"
3639
# Order statuses 0 and 1: Vehicle is at the dealership
3740
# 2, 3, 4, and 5: Vehicle is in transit or in production"
3841
+ ', statuses:["0","1","2","3","4","5"] }, sorting: [{order: ASC, criteria: DISTANCE_TO_LOCATOR_ZIP},{order:ASC,criteria:PRICE}] pagination: {pageIndex: 1, ' # noqa: B950

src/routers/ford.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ async def main(
9393
}
9494
# Retrieve the initial batch of 12 vehicles
9595
inv = await http.get(uri=inventory_uri, headers=headers, params=inventory_params)
96-
print(f"\n\n\n{type(inv)}\n\n\n")
9796
try:
9897
inv = inv.json()
9998
except AttributeError:

src/routers/genesis.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1-
from fastapi import APIRouter, Depends, Request
1+
# Copyright 2023 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+
import datetime
217

18+
from fastapi import APIRouter, Depends, Request
319
from src.libs.common_query_params import CommonInventoryQueryParams
4-
from src.libs.responses import error_response, send_response
520
from src.libs.http import AsyncHTTPClient
21+
from src.libs.responses import error_response, send_response
622

723
router = APIRouter(prefix="/api")
824
verify_ssl = True
@@ -15,15 +31,26 @@ async def get_genesis_inventory(
1531
common_params: CommonInventoryQueryParams = Depends(),
1632
) -> dict:
1733
"""Makes a request to the Genesis Inventory API and returns a JSON object containing
18-
the inventory results for a given vehicle model, zip code and search radius.
34+
the inventory results for a given vehicle model and zip code.
35+
36+
The Genesis API does not accept a search radius nor a model year, rather a maxdealers
37+
URI path segment. The EV Finder frontend deals with filtering the results to display
38+
the relevant information for the search performed.
39+
40+
Args:
41+
req (Request): The HTTP request from the EV Finder application.
42+
common_params (CommonInventoryQueryParams, optional): The EV Finder query params.
43+
Typically zip, year, model and radius. Defaults to Depends().
44+
45+
Returns:
46+
dict: A JSON object containing the inventory results for the given search.
1947
"""
20-
params = {
21-
"zip": common_params.zip,
22-
"year": common_params.year,
23-
"modelname": common_params.model,
24-
"radius": common_params.radius,
25-
"maxdealers": 25,
26-
}
48+
49+
zipcode = common_params.zip
50+
modelname = common_params.model
51+
maxdealers = 50
52+
todays_date = datetime.datetime.now().strftime("%Y-%m-%d")
53+
2754
headers = {
2855
"User-Agent": req.headers.get("User-Agent"),
2956
"Referer": f"{genesis_base_url}/us/en/new/inventory.html",
@@ -33,9 +60,10 @@ async def get_genesis_inventory(
3360
base_url=genesis_base_url, timeout_value=30.0, verify=verify_ssl
3461
) as http:
3562
inv = await http.get(
36-
uri=("/bin/api/v1/inventory"),
63+
uri=(
64+
f"/bin/api/v1/inventory.json/{modelname}/{zipcode}/{maxdealers}/{todays_date}"
65+
),
3766
headers=headers,
38-
params=params,
3967
)
4068

4169
inventory_data = inv.json()
@@ -45,7 +73,9 @@ async def get_genesis_inventory(
4573
response_data=inventory_data,
4674
)
4775
else:
48-
error_message = "An error occurred with the Genesis API"
76+
error_message = (
77+
"An error occurred obtaining vehicle inventory for this search."
78+
)
4979
return error_response(
5080
error_message=error_message, error_data=inventory_data
5181
)
@@ -79,5 +109,7 @@ async def get_genesis_vin_detail(req: Request) -> dict:
79109
response_data=vin_data,
80110
)
81111
else:
82-
error_message = "An error occurred with the Genesis API"
112+
error_message = (
113+
"An error occurred obtaining VIN information for this vehicle."
114+
)
83115
return error_response(error_message=error_message, error_data=vin_data)

src/routers/helpers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import APIRouter, Path
44

55
from src.libs.responses import error_response, send_response
6+
from src.libs.http import AsyncHTTPClient
67

78
router = APIRouter(prefix="/api")
89
verify_ssl = True
@@ -29,3 +30,31 @@ def send_error_response(
2930
return error_response(
3031
error_message=f"This is a {status_code} error", status_code=status_code
3132
)
33+
34+
35+
@router.get("/test/status/{status_code}")
36+
async def send_httpstatus_error_response(
37+
status_code: int = Path(
38+
title="A HTTP status code in the 400 or 500 class", ge=400, le=599
39+
),
40+
):
41+
async with AsyncHTTPClient(
42+
base_url="https://httpstat.us", timeout_value=1.0, verify=True
43+
) as http:
44+
params = {"sleep": 2000}
45+
headers = {"User-Agent": "Test"}
46+
h = await http.get(uri=f"/{status_code}", params=params, headers=headers)
47+
48+
try:
49+
h.json()
50+
return error_response(
51+
error_message=f"This is a error: {h}", status_code=status_code
52+
)
53+
except AttributeError:
54+
return error_response(
55+
error_message=f"This is a error: {h}", status_code=status_code
56+
)
57+
58+
# return "foo"
59+
# return h.text if h.text else "foo"
60+
#

src/routers/kia.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,10 @@ async def get_kia_inventory(
5252
error_data=inv.text,
5353
)
5454

55+
# When this key is not present, no vehicles were found for the given search so return
56+
# an empty dict instead of the Kia response (which is filled with nulls)
5557
try:
5658
data["inventoryVehicles"]
5759
return send_response(response_data=data)
5860
except KeyError:
59-
return error_response(
60-
error_message="Invalid data received from the Kia inventory system.",
61-
error_data=data,
62-
status_code=500,
63-
)
61+
return send_response(response_data={})

src/tests/conftest.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
import time
3+
4+
import vcr
5+
6+
cassette_dir = "tests/cassettes"
7+
8+
9+
def program_vcr():
10+
for cassette in os.listdir(cassette_dir):
11+
if "yaml" in cassette: # Only delete the casette files
12+
delete_stale_cassette(cassette_name=cassette)
13+
14+
_vcr = vcr.VCR(
15+
cassette_library_dir=cassette_dir,
16+
record_mode="new_episodes",
17+
)
18+
19+
return _vcr
20+
21+
22+
def delete_stale_cassette(
23+
cassette_name: str, delete_if_older_than_days: int = 30
24+
) -> None:
25+
"""We're using VCR.py to record the request / responses to the various manufacturer
26+
APIs, in order to facilitate offline testing, less-flakey and deterministic tests.
27+
As the manufacturer APIs are not under our control, I want to occasionally refresh
28+
the API responses to ensure our tests pass against the most recent version of a
29+
manufacturer API. If a VCR.py cassette is > delete_if_older_than_days days old,
30+
remove it before starting a test.
31+
32+
Args:
33+
cassette_name (str): Name of the cassette file to delete.
34+
delete_if_older_than_days (int, optional): Delete a cassette file if older than
35+
this value. Defaults to 30.
36+
"""
37+
38+
cassette_file = f"{cassette_dir}/{cassette_name}"
39+
file_age_in_sec = time.time() - os.path.getmtime(cassette_file)
40+
41+
if file_age_in_sec > (delete_if_older_than_days * 60 * 60):
42+
print(f"Deleting {cassette_file}")
43+
os.remove(cassette_file)

0 commit comments

Comments
 (0)