Skip to content

Commit 43ed3e1

Browse files
committed
v0.4.2
1 parent 8e21da7 commit 43ed3e1

File tree

12 files changed

+2285
-39
lines changed

12 files changed

+2285
-39
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## v0.42 (2024-02-24)
4+
5+
[GitHub release](https://github.com/davidpeckham/vin/releases/tag/v0.4.2)
6+
7+
### Fixes
8+
9+
* Uses WMI to find vehicle make when VIN is incomplete or incorrect.
10+
* Series and trim decoding is more reliable
11+
312
## v0.41 (2024-02-21)
413

514
[GitHub release](https://github.com/davidpeckham/vin/releases/tag/v0.4.1)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
## Why use VIN?
4343

4444
- **Accurate** — Uses U.S. National Highway Traffic Safety Administration vehicle data.
45-
- **Fast** — Validate and decode 1,500 VINs per second.
45+
- **Fast** — Validate and decode hundreds of VINs per second.
4646

4747
## Installation
4848

src/vin/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-FileCopyrightText: 2024-present David Peckham <dave.peckham@icloud.com>
22
#
33
# SPDX-License-Identifier: MIT
4-
__version__ = "0.4.1"
4+
__version__ = "0.4.2"

src/vin/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from vin.constants import VIN_LENGTH
1919
from vin.constants import VIN_MODEL_YEAR_CHARACTERS
2020
from vin.constants import VIN_POSITION_WEIGHTS
21-
from vin.database import lookup_vehicle
21+
from vin.database import decode_vin
2222

2323

2424
class DecodingError(Exception):
@@ -568,11 +568,11 @@ def _decode_vin(self) -> None:
568568
"""
569569
model_year = self._decode_model_year()
570570
if model_year > 0:
571-
vehicle = lookup_vehicle(self.wmi, self.descriptor, model_year)
571+
vehicle = decode_vin(self.wmi, self.descriptor, model_year)
572572
else:
573-
vehicle = lookup_vehicle(self.wmi, self.descriptor, abs(model_year))
573+
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year))
574574
if not vehicle:
575-
vehicle = lookup_vehicle(self.wmi, self.descriptor, abs(model_year) - 30)
575+
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year) - 30)
576576
if vehicle is None:
577577
raise DecodingError()
578578

src/vin/database.py

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import sqlite3
44
from importlib.resources import files
5+
from typing import Any
56

67

78
log = logging.getLogger(__name__)
@@ -22,7 +23,7 @@ def regex(value, pattern) -> bool:
2223

2324

2425
def query(sql: str, args: tuple = ()) -> list[sqlite3.Row]:
25-
"""insert rows and return rowcount"""
26+
"""query the database and return results"""
2627
cursor = connection.cursor()
2728
results = cursor.execute(sql, args).fetchall()
2829
cursor.close()
@@ -38,7 +39,23 @@ def get_wmis_for_cars_and_light_trucks() -> list[str]:
3839
return [result["wmi"] for result in query(sql=GET_WMI_FOR_CARS_AND_LIGHT_TRUCKS)]
3940

4041

41-
def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
42+
GET_WMI_FOR_CARS_AND_LIGHT_TRUCKS = """
43+
select
44+
wmi.code as wmi
45+
from
46+
wmi
47+
where
48+
vehicle_type_id in (2, 7) -- Cars and MPVs
49+
or ( -- light trucks
50+
wmi.vehicle_type_id = 3
51+
and wmi.truck_type_id = 1
52+
)
53+
order by
54+
wmi.code;
55+
"""
56+
57+
58+
def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
4259
"""get vehicle details
4360
4461
Args:
@@ -47,14 +64,8 @@ def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
4764
Returns:
4865
Vehicle: the vehicle details
4966
"""
50-
if results := query(sql=LOOKUP_VEHICLE_SQL, args=(wmi, model_year, vds)):
51-
details = {
52-
# "series": None,
53-
# "trim": None,
54-
"model_year": model_year,
55-
# "body_class": None,
56-
# "electrification_level": None,
57-
}
67+
if results := query(sql=DECODE_VIN_SQL, args=(wmi, model_year, vds)):
68+
details: dict[str, Any] = {"model_year": model_year}
5869
for row in results:
5970
details.update(
6071
{
@@ -71,15 +82,20 @@ def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
7182
"truck_type",
7283
"vehicle_type",
7384
]
74-
if row[k] is not None
85+
if k not in details and row[k] is not None
7586
}
7687
)
88+
if "make" not in details: # noqa: SIM102
89+
if make := get_make_from_wmi(wmi):
90+
details["make"] = make
7791
return details
7892
return None
7993

8094

81-
LOOKUP_VEHICLE_SQL = """
95+
DECODE_VIN_SQL = """
8296
select
97+
pattern.id,
98+
pattern.vds,
8399
manufacturer.name as manufacturer,
84100
make.name as make,
85101
model.name as model,
@@ -93,35 +109,49 @@ def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
93109
from
94110
pattern
95111
join manufacturer on manufacturer.id = pattern.manufacturer_id
112+
join wmi on wmi.code = pattern.wmi
113+
join vehicle_type on vehicle_type.id = wmi.vehicle_type_id
114+
left join truck_type on truck_type.id = wmi.truck_type_id
115+
left join country on country.alpha_2_code = wmi.country
96116
left join make_model on make_model.model_id = pattern.model_id
97117
left join make on make.id = make_model.make_id
118+
-- left join make as default_make on default_make.id = wmi.make_id
98119
left join model on model.id = pattern.model_id
99120
left join series on series.id = pattern.series_id
100121
left join trim on trim.id = pattern.trim_id
101-
join wmi on wmi.code = pattern.wmi
102-
join vehicle_type on vehicle_type.id = wmi.vehicle_type_id
103-
left join truck_type on truck_type.id = wmi.truck_type_id
104-
left join country on country.alpha_2_code = wmi.country
105122
left join body_class on body_class.id = pattern.body_class_id
106123
left join electrification_level on electrification_level.id = pattern.electrification_level_id
107124
where
108125
pattern.wmi = ?
109126
and ? between pattern.from_year and pattern.to_year
110-
and REGEXP (?, pattern.vds);
127+
and REGEXP (?, pattern.vds)
128+
order by
129+
pattern.from_year desc,
130+
coalesce(pattern.updated, pattern.created) desc,
131+
pattern.id asc;
111132
"""
133+
"""Sort order is important. Best match and most recent patterns on top."""
112134

113135

114-
GET_WMI_FOR_CARS_AND_LIGHT_TRUCKS = """
136+
def get_make_from_wmi(wmi: str) -> str:
137+
"""Get the name of the make produced by a WMI. Used when the VIN is wrong or incomplete.
138+
139+
Returns:
140+
str: Returns the name of the single make produced by this WMI. If the WMI \
141+
produces more than one make, returns empty string.
142+
"""
143+
make = ""
144+
if results := query(sql=GET_MAKE_FROM_WMI_SQL, args=(wmi,)):
145+
make = results[0]["make"]
146+
return make
147+
148+
149+
GET_MAKE_FROM_WMI_SQL = """
115150
select
116-
wmi.code as wmi
151+
make.name as make
117152
from
118153
wmi
154+
join make on make.id = wmi.make_id
119155
where
120-
vehicle_type_id in (2, 7) -- Cars and MPVs
121-
or ( -- light trucks
122-
wmi.vehicle_type_id = 3
123-
and wmi.truck_type_id = 1
124-
)
125-
order by
126-
wmi.code;
156+
wmi.code == ?;
127157
"""

src/vin/vehicle.db

22.3 MB
Binary file not shown.

tests/benchmarks/test_benchmark.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ def test_benchmark_100_vins(benchmark, hundred_vehicles):
1212
assert result
1313

1414

15-
def test_benchmark_1000_vins(benchmark, thousand_vehicles):
16-
result = benchmark(decode_vins, thousand_vehicles)
17-
assert result
15+
# def test_benchmark_1000_vins(benchmark, thousand_vehicles):
16+
# result = benchmark(decode_vins, thousand_vehicles)
17+
# assert result

tests/cars/test_decode.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_no_electrification_level() -> None:
4242

4343
def test_missing_make() -> None:
4444
v = VIN("JTDBAMDE2MJ008197")
45-
assert v.make == ""
45+
assert v.make == "Toyota"
4646

4747

4848
def test_missing_model() -> None:
@@ -52,7 +52,7 @@ def test_missing_model() -> None:
5252

5353
def test_vpic_data_is_incomplete() -> None:
5454
v = VIN("1G1F76E04K4140798")
55-
assert v.make == ""
55+
assert v.make == "Chevrolet"
5656

5757

5858
@pytest.mark.xfail(reason="vPIC dbo.Pattern seems to confuse the 1993 Integra and Legend trim data")
@@ -65,3 +65,44 @@ def test_vin_schema_collision() -> None:
6565
def test_wrong_trim_eclipse() -> None:
6666
v = VIN("4A3AK24F36E026691")
6767
assert v.trim == "LOW"
68+
69+
70+
def test_incorrect_vin():
71+
v = VIN("4T1B21HK0MU016210")
72+
assert v.make == "Toyota"
73+
74+
75+
def test_incomplete_vin1():
76+
v = VIN("JTDBBRBE9LJ009553")
77+
assert v.make == "Toyota"
78+
79+
80+
def test_incomplete_vin2():
81+
assert VIN("1G1F76E04K4140798").make == "Chevrolet"
82+
assert VIN("1HGCV2634LA600001").make == "Honda"
83+
assert VIN("3HGGK5H8XLM725852").trim == "EX, EX-L"
84+
assert VIN("4T1B21HK0MU016210").make == "Toyota"
85+
assert VIN("4T1B21HK3MU015245").make == "Toyota"
86+
assert VIN("4T1F31AK3LU531161").trim == "XLE"
87+
assert VIN("4T1F31AK5LU535373").trim == "XLE"
88+
assert VIN("4T1F31AK7LU010816").trim == "XLE"
89+
assert VIN("5TDEBRCH0MS058490").series == "AXUH78L"
90+
assert VIN("5TDEBRCH4MS043703").series == "AXUH78L"
91+
assert VIN("5TDEBRCH8MS019761").series == "AXUH78L"
92+
assert VIN("5TDEBRCH9MS031126").series == "AXUH78L"
93+
assert VIN("5TDEBRCHXMS017204").series == "AXUH78L"
94+
assert VIN("5TDGBRCH7MS038701").series == "AXUH78L"
95+
assert VIN("5TDHBRCH0MS065999").series == "AXUH78L"
96+
assert VIN("JTDBAMDE2MJ008197").make == "Toyota"
97+
assert VIN("JTDBBRBE9LJ009553").make == "Toyota"
98+
assert VIN("JTDBBRBE9LJ009553").make == "Toyota"
99+
assert VIN("JTMFB3FV7MD049459").trim == "LE"
100+
assert VIN("KMHLN4AJ3MU004776").series == "SEL"
101+
assert VIN("KMHLN4AJ5MU009817").series == "SEL"
102+
assert VIN("WAUHJGFF8F1120794").trim == "Prestige S-Line Auto/Technik S-Line Auto (Canada)"
103+
assert VIN("WAUHJGFF9F1065644").trim == "Prestige S-Line Auto/Technik S-Line Auto (Canada)"
104+
105+
106+
@pytest.mark.xfail(reason="downloadable snapshot returns SE, online vPIC returns SE (AQ301 Trans)")
107+
def test_snapshot_is_behind_online_vpic():
108+
assert VIN("3VW7M7BU2RM018616").trim == "SE, SE (AQ301 Trans)"

tests/cars/test_decode.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ test_decode:
4242
- vin: WAULFAFR6DA001366
4343
model_year: 2013
4444
make: Audi
45-
model: A5 Premium Plus Quattro
45+
model: A5 Premium Plus quattro
4646
body_class:
4747
- vin: 1G8ZH1278XZ108219
4848
model_year: 1999
@@ -458,7 +458,7 @@ test_decode:
458458
- vin: WAULFAFR8DA002471
459459
model_year: 2013
460460
make: Audi
461-
model: A5 Premium Plus Quattro
461+
model: A5 Premium Plus quattro
462462
body_class:
463463
- vin: JH4KA3172KC019247
464464
model_year: 1989

tests/cars/test_trim.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
from vin import VIN
3+
4+
5+
def test_2020_honda_fit() -> None:
6+
v = VIN("3HGGK5H8XLM725852")
7+
assert v.model_year == 2020
8+
assert v.make == "Honda"
9+
assert v.model == "Fit"
10+
assert v.series == ""
11+
assert v.trim == "EX, EX-L"
12+
assert v.body_class == "Hatchback/Liftback/Notchback"
13+
14+
15+
def test_2021_toyota_rav4() -> None:
16+
v = VIN("JTMFB3FV7MD049459")
17+
assert v.model_year == 2021
18+
assert v.make == "Toyota"
19+
assert v.model == "RAV4 Prime"
20+
assert v.series == "AXAP54L"
21+
assert v.trim == "LE"
22+
assert v.body_class == "Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV)"
23+
24+
25+
@pytest.mark.skip(
26+
"This trim isn't in the February 16, 2024 vPIC snapshot. Check the next snapshot."
27+
)
28+
def test_2024_vw_jetta() -> None:
29+
"""Trim is SE in the vPIC 2024-02-16 snapshot. Check this when vPIC
30+
releases a newer snapshot.
31+
"""
32+
v = VIN("3VW7M7BU2RM018616")
33+
assert v.model_year == 2024
34+
assert v.make == "Volkswagen"
35+
assert v.model == "Jetta"
36+
assert v.series == ""
37+
assert v.trim == "SE, SE (AQ301 Trans)"
38+
assert v.body_class == "Sedan/Saloon"
39+
40+
41+
def test_2021_toyota_highlander() -> None:
42+
v = VIN("5TDHBRCH0MS065999")
43+
assert v.model_year == 2021
44+
assert v.make == "Toyota"
45+
assert v.model == "Highlander"
46+
assert v.series == "AXUH78L"
47+
assert v.trim == "XLE Nav"
48+
assert v.body_class == "Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV)"
49+
assert v.electrification_level == "Strong HEV (Hybrid Electric Vehicle)"

tests/test_vin_exceptions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import parametrize_from_file
2+
import pytest
3+
from vin import VIN
4+
5+
6+
@pytest.mark.xfail(reason="NHTSA VIN exceptions aren't supported yet")
7+
@parametrize_from_file
8+
def test_vin_exceptions(
9+
vin: str,
10+
model_year: int,
11+
make: str,
12+
model: str,
13+
series: str,
14+
body_class: str,
15+
electrification_level: str,
16+
) -> None:
17+
v = VIN(vin)
18+
assert v.model_year == model_year
19+
assert v.make == make
20+
assert v.model == model
21+
assert v.series == series
22+
assert v.body_class == body_class
23+
assert v.electrification_level == electrification_level

0 commit comments

Comments
 (0)