Skip to content

Commit

Permalink
v0.4.2
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpeckham committed Feb 24, 2024
1 parent 8e21da7 commit 43ed3e1
Show file tree
Hide file tree
Showing 12 changed files with 2,285 additions and 39 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v0.42 (2024-02-24)

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

### Fixes

* Uses WMI to find vehicle make when VIN is incomplete or incorrect.
* Series and trim decoding is more reliable

## v0.41 (2024-02-21)

[GitHub release](https://github.com/davidpeckham/vin/releases/tag/v0.4.1)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
## Why use VIN?

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

## Installation

Expand Down
2 changes: 1 addition & 1 deletion src/vin/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-present David Peckham <[email protected]>
#
# SPDX-License-Identifier: MIT
__version__ = "0.4.1"
__version__ = "0.4.2"
8 changes: 4 additions & 4 deletions src/vin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from vin.constants import VIN_LENGTH
from vin.constants import VIN_MODEL_YEAR_CHARACTERS
from vin.constants import VIN_POSITION_WEIGHTS
from vin.database import lookup_vehicle
from vin.database import decode_vin


class DecodingError(Exception):
Expand Down Expand Up @@ -568,11 +568,11 @@ def _decode_vin(self) -> None:
"""
model_year = self._decode_model_year()
if model_year > 0:
vehicle = lookup_vehicle(self.wmi, self.descriptor, model_year)
vehicle = decode_vin(self.wmi, self.descriptor, model_year)
else:
vehicle = lookup_vehicle(self.wmi, self.descriptor, abs(model_year))
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year))
if not vehicle:
vehicle = lookup_vehicle(self.wmi, self.descriptor, abs(model_year) - 30)
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year) - 30)
if vehicle is None:
raise DecodingError()

Expand Down
82 changes: 56 additions & 26 deletions src/vin/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
import sqlite3
from importlib.resources import files
from typing import Any


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


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


def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
GET_WMI_FOR_CARS_AND_LIGHT_TRUCKS = """
select
wmi.code as wmi
from
wmi
where
vehicle_type_id in (2, 7) -- Cars and MPVs
or ( -- light trucks
wmi.vehicle_type_id = 3
and wmi.truck_type_id = 1
)
order by
wmi.code;
"""


def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
"""get vehicle details
Args:
Expand All @@ -47,14 +64,8 @@ def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
Returns:
Vehicle: the vehicle details
"""
if results := query(sql=LOOKUP_VEHICLE_SQL, args=(wmi, model_year, vds)):
details = {
# "series": None,
# "trim": None,
"model_year": model_year,
# "body_class": None,
# "electrification_level": None,
}
if results := query(sql=DECODE_VIN_SQL, args=(wmi, model_year, vds)):
details: dict[str, Any] = {"model_year": model_year}
for row in results:
details.update(
{
Expand All @@ -71,15 +82,20 @@ def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
"truck_type",
"vehicle_type",
]
if row[k] is not None
if k not in details and row[k] is not None
}
)
if "make" not in details: # noqa: SIM102
if make := get_make_from_wmi(wmi):
details["make"] = make
return details
return None


LOOKUP_VEHICLE_SQL = """
DECODE_VIN_SQL = """
select
pattern.id,
pattern.vds,
manufacturer.name as manufacturer,
make.name as make,
model.name as model,
Expand All @@ -93,35 +109,49 @@ def lookup_vehicle(wmi: str, vds: str, model_year: int) -> dict | None:
from
pattern
join manufacturer on manufacturer.id = pattern.manufacturer_id
join wmi on wmi.code = pattern.wmi
join vehicle_type on vehicle_type.id = wmi.vehicle_type_id
left join truck_type on truck_type.id = wmi.truck_type_id
left join country on country.alpha_2_code = wmi.country
left join make_model on make_model.model_id = pattern.model_id
left join make on make.id = make_model.make_id
-- left join make as default_make on default_make.id = wmi.make_id
left join model on model.id = pattern.model_id
left join series on series.id = pattern.series_id
left join trim on trim.id = pattern.trim_id
join wmi on wmi.code = pattern.wmi
join vehicle_type on vehicle_type.id = wmi.vehicle_type_id
left join truck_type on truck_type.id = wmi.truck_type_id
left join country on country.alpha_2_code = wmi.country
left join body_class on body_class.id = pattern.body_class_id
left join electrification_level on electrification_level.id = pattern.electrification_level_id
where
pattern.wmi = ?
and ? between pattern.from_year and pattern.to_year
and REGEXP (?, pattern.vds);
and REGEXP (?, pattern.vds)
order by
pattern.from_year desc,
coalesce(pattern.updated, pattern.created) desc,
pattern.id asc;
"""
"""Sort order is important. Best match and most recent patterns on top."""


GET_WMI_FOR_CARS_AND_LIGHT_TRUCKS = """
def get_make_from_wmi(wmi: str) -> str:
"""Get the name of the make produced by a WMI. Used when the VIN is wrong or incomplete.
Returns:
str: Returns the name of the single make produced by this WMI. If the WMI \
produces more than one make, returns empty string.
"""
make = ""
if results := query(sql=GET_MAKE_FROM_WMI_SQL, args=(wmi,)):
make = results[0]["make"]
return make


GET_MAKE_FROM_WMI_SQL = """
select
wmi.code as wmi
make.name as make
from
wmi
join make on make.id = wmi.make_id
where
vehicle_type_id in (2, 7) -- Cars and MPVs
or ( -- light trucks
wmi.vehicle_type_id = 3
and wmi.truck_type_id = 1
)
order by
wmi.code;
wmi.code == ?;
"""
Binary file modified src/vin/vehicle.db
Binary file not shown.
6 changes: 3 additions & 3 deletions tests/benchmarks/test_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ def test_benchmark_100_vins(benchmark, hundred_vehicles):
assert result


def test_benchmark_1000_vins(benchmark, thousand_vehicles):
result = benchmark(decode_vins, thousand_vehicles)
assert result
# def test_benchmark_1000_vins(benchmark, thousand_vehicles):
# result = benchmark(decode_vins, thousand_vehicles)
# assert result
45 changes: 43 additions & 2 deletions tests/cars/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_no_electrification_level() -> None:

def test_missing_make() -> None:
v = VIN("JTDBAMDE2MJ008197")
assert v.make == ""
assert v.make == "Toyota"


def test_missing_model() -> None:
Expand All @@ -52,7 +52,7 @@ def test_missing_model() -> None:

def test_vpic_data_is_incomplete() -> None:
v = VIN("1G1F76E04K4140798")
assert v.make == ""
assert v.make == "Chevrolet"


@pytest.mark.xfail(reason="vPIC dbo.Pattern seems to confuse the 1993 Integra and Legend trim data")
Expand All @@ -65,3 +65,44 @@ def test_vin_schema_collision() -> None:
def test_wrong_trim_eclipse() -> None:
v = VIN("4A3AK24F36E026691")
assert v.trim == "LOW"


def test_incorrect_vin():
v = VIN("4T1B21HK0MU016210")
assert v.make == "Toyota"


def test_incomplete_vin1():
v = VIN("JTDBBRBE9LJ009553")
assert v.make == "Toyota"


def test_incomplete_vin2():
assert VIN("1G1F76E04K4140798").make == "Chevrolet"
assert VIN("1HGCV2634LA600001").make == "Honda"
assert VIN("3HGGK5H8XLM725852").trim == "EX, EX-L"
assert VIN("4T1B21HK0MU016210").make == "Toyota"
assert VIN("4T1B21HK3MU015245").make == "Toyota"
assert VIN("4T1F31AK3LU531161").trim == "XLE"
assert VIN("4T1F31AK5LU535373").trim == "XLE"
assert VIN("4T1F31AK7LU010816").trim == "XLE"
assert VIN("5TDEBRCH0MS058490").series == "AXUH78L"
assert VIN("5TDEBRCH4MS043703").series == "AXUH78L"
assert VIN("5TDEBRCH8MS019761").series == "AXUH78L"
assert VIN("5TDEBRCH9MS031126").series == "AXUH78L"
assert VIN("5TDEBRCHXMS017204").series == "AXUH78L"
assert VIN("5TDGBRCH7MS038701").series == "AXUH78L"
assert VIN("5TDHBRCH0MS065999").series == "AXUH78L"
assert VIN("JTDBAMDE2MJ008197").make == "Toyota"
assert VIN("JTDBBRBE9LJ009553").make == "Toyota"
assert VIN("JTDBBRBE9LJ009553").make == "Toyota"
assert VIN("JTMFB3FV7MD049459").trim == "LE"
assert VIN("KMHLN4AJ3MU004776").series == "SEL"
assert VIN("KMHLN4AJ5MU009817").series == "SEL"
assert VIN("WAUHJGFF8F1120794").trim == "Prestige S-Line Auto/Technik S-Line Auto (Canada)"
assert VIN("WAUHJGFF9F1065644").trim == "Prestige S-Line Auto/Technik S-Line Auto (Canada)"


@pytest.mark.xfail(reason="downloadable snapshot returns SE, online vPIC returns SE (AQ301 Trans)")
def test_snapshot_is_behind_online_vpic():
assert VIN("3VW7M7BU2RM018616").trim == "SE, SE (AQ301 Trans)"
4 changes: 2 additions & 2 deletions tests/cars/test_decode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test_decode:
- vin: WAULFAFR6DA001366
model_year: 2013
make: Audi
model: A5 Premium Plus Quattro
model: A5 Premium Plus quattro
body_class:
- vin: 1G8ZH1278XZ108219
model_year: 1999
Expand Down Expand Up @@ -458,7 +458,7 @@ test_decode:
- vin: WAULFAFR8DA002471
model_year: 2013
make: Audi
model: A5 Premium Plus Quattro
model: A5 Premium Plus quattro
body_class:
- vin: JH4KA3172KC019247
model_year: 1989
Expand Down
49 changes: 49 additions & 0 deletions tests/cars/test_trim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest
from vin import VIN


def test_2020_honda_fit() -> None:
v = VIN("3HGGK5H8XLM725852")
assert v.model_year == 2020
assert v.make == "Honda"
assert v.model == "Fit"
assert v.series == ""
assert v.trim == "EX, EX-L"
assert v.body_class == "Hatchback/Liftback/Notchback"


def test_2021_toyota_rav4() -> None:
v = VIN("JTMFB3FV7MD049459")
assert v.model_year == 2021
assert v.make == "Toyota"
assert v.model == "RAV4 Prime"
assert v.series == "AXAP54L"
assert v.trim == "LE"
assert v.body_class == "Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV)"


@pytest.mark.skip(
"This trim isn't in the February 16, 2024 vPIC snapshot. Check the next snapshot."
)
def test_2024_vw_jetta() -> None:
"""Trim is SE in the vPIC 2024-02-16 snapshot. Check this when vPIC
releases a newer snapshot.
"""
v = VIN("3VW7M7BU2RM018616")
assert v.model_year == 2024
assert v.make == "Volkswagen"
assert v.model == "Jetta"
assert v.series == ""
assert v.trim == "SE, SE (AQ301 Trans)"
assert v.body_class == "Sedan/Saloon"


def test_2021_toyota_highlander() -> None:
v = VIN("5TDHBRCH0MS065999")
assert v.model_year == 2021
assert v.make == "Toyota"
assert v.model == "Highlander"
assert v.series == "AXUH78L"
assert v.trim == "XLE Nav"
assert v.body_class == "Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV)"
assert v.electrification_level == "Strong HEV (Hybrid Electric Vehicle)"
23 changes: 23 additions & 0 deletions tests/test_vin_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import parametrize_from_file
import pytest
from vin import VIN


@pytest.mark.xfail(reason="NHTSA VIN exceptions aren't supported yet")
@parametrize_from_file
def test_vin_exceptions(
vin: str,
model_year: int,
make: str,
model: str,
series: str,
body_class: str,
electrification_level: str,
) -> None:
v = VIN(vin)
assert v.model_year == model_year
assert v.make == make
assert v.model == model
assert v.series == series
assert v.body_class == body_class
assert v.electrification_level == electrification_level
Loading

0 comments on commit 43ed3e1

Please sign in to comment.