Skip to content

Commit

Permalink
v0.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpeckham committed Mar 5, 2024
1 parent c58e4ab commit 6078875
Show file tree
Hide file tree
Showing 11 changed files with 111,295 additions and 1,515 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.5.0 (2024-03-04)

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

### New Features

* Decodes VINs that have an incorrect or zero model year character (partially resolves https://github.com/davidpeckham/vin/issues/2, sshane).
* More unit tests

## v0.4.3 (2024-02-25)

[GitHub release](https://github.com/davidpeckham/vin/releases/tag/v0.4.3)
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.3"
__version__ = "0.5.0"
42 changes: 26 additions & 16 deletions src/vin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
#
# SPDX-License-Identifier: MIT

"""A Vehicle Identification Number (VIN).
"""
"""A Vehicle Identification Number (VIN)."""

# ruff: noqa: TRY003, EM101, EM102

Expand Down Expand Up @@ -74,14 +72,22 @@ class VIN:
"""

def __init__(self, vin: str, decode: bool = True, fix_check_digit: bool = False) -> None:
def __init__(
self,
vin: str,
decode: bool = True,
fix_check_digit: bool = False,
decode_model_year: bool = True,
) -> None:
"""Validates the VIN and decodes vehicle information.
Args:
vin: The 17-digit Vehicle Identification Number.
decode: Decode vehicle details from the NHTSA vPIC database
fix_check_digit: If True, fix an incorrect check digit
instead of raising a ValueError.
fix_check_digit: If True, fix an incorrect check digit instead of raising a ValueError.
decode_model_year: If True, validate the model year character. If False, ignore the \
model year character, which can be useful for vehicles manufactured for markets \
outside North America (though results may be incomplete or inaccurate).
Raises:
TypeError: `vin` is not a string.
Expand All @@ -94,7 +100,7 @@ def __init__(self, vin: str, decode: bool = True, fix_check_digit: bool = False)
raise TypeError("VIN must be a string")
if len(vin) != VIN_LENGTH:
raise ValueError(f"VIN must be exactly {VIN_LENGTH} characters long")
if vin[9] not in VIN_MODEL_YEAR_CHARACTERS:
if decode_model_year and vin[9] not in VIN_MODEL_YEAR_CHARACTERS:
raise ValueError(
"VIN model year character must be one of these characters "
f"{VIN_MODEL_YEAR_CHARACTERS}"
Expand All @@ -111,7 +117,7 @@ def __init__(self, vin: str, decode: bool = True, fix_check_digit: bool = False)

self._vin: str = vin
if decode:
self._decode_vin()
self._decode_vin(decode_model_year)
return

@property
Expand Down Expand Up @@ -527,7 +533,7 @@ def _decode_model_year(self) -> int:
-2025
"""
year_code = self._vin[9]
assert year_code in VIN_MODEL_YEAR_CHARACTERS
# assert year_code in VIN_MODEL_YEAR_CHARACTERS
model_year = 0
conclusive = False

Expand Down Expand Up @@ -558,7 +564,7 @@ def _decode_model_year(self) -> int:

return model_year if conclusive else -model_year

def _decode_vin(self) -> None:
def _decode_vin(self, decode_model_year=True) -> None:
"""decode the VIN to get manufacturer, make, model, and other vehicle details
Args:
Expand All @@ -567,13 +573,17 @@ def _decode_vin(self) -> None:
Raises:
DecodingError: Unable to decode VIN using NHTSA vPIC.
"""
model_year = self._decode_model_year()
if model_year > 0:
vehicle = decode_vin(self.wmi, self.descriptor, model_year)
if decode_model_year:
model_year = self._decode_model_year()
if model_year > 0:
vehicle = decode_vin(self.wmi, self.descriptor, model_year)
else:
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year))
if not vehicle:
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year) - 30)
else:
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year))
if not vehicle:
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year) - 30)
vehicle = decode_vin(self.wmi, self.descriptor)

if vehicle is None:
raise DecodingError()

Expand Down
51 changes: 49 additions & 2 deletions src/vin/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,24 @@ def get_wmis_for_cars_and_light_trucks() -> list[str]:
"""


def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
def decode_vin(wmi: str, vds: str, model_year: int | None = None) -> dict | None:
"""get vehicle details
Args:
vin: The 17-digit Vehicle Identification Number.
model_year: The vehicle model year. Outside North America, the VIN model year
character may always be set to zero. When model_year is None, we will try
to decode the VIN, but the information it returns may not be accurate.
Returns:
Vehicle: the vehicle details
"""
if results := query(sql=DECODE_VIN_SQL, args=(wmi, model_year, vds)):
if model_year is not None:
results = query(sql=DECODE_VIN_SQL, args=(wmi, model_year, vds))
else:
results = query(sql=DECODE_VIN_WITHOUT_MODEL_YEAR_SQL, args=(wmi, vds))

if results:
details: dict[str, Any] = {"model_year": model_year}
for row in results:
details.update(
Expand All @@ -89,6 +97,7 @@ def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
if make := get_make_from_wmi(wmi):
details["make"] = make
return details

return None


Expand Down Expand Up @@ -131,6 +140,44 @@ def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
"""
"""Sort order is important. Best match and most recent patterns on top."""

DECODE_VIN_WITHOUT_MODEL_YEAR_SQL = """
select
pattern.id,
pattern.vds,
manufacturer.name as manufacturer,
make.name as make,
model.name as model,
series.name as series,
trim.name as trim,
vehicle_type.name as vehicle_type,
truck_type.name as truck_type,
country.name as country,
body_class.name as body_class,
electrification_level.name as electrification_level
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 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
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 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."""


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.
Expand Down
13 changes: 13 additions & 0 deletions tests/cars/test_europe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from vin import VIN


def test_taiwan():
v = VIN("JTHBYLFF305000302", fix_check_digit=True, decode_model_year=False)
assert v.make == "Lexus"
assert v.model == "LS"


def test_italy():
v = VIN("JTMW53FV60D023016", fix_check_digit=True, decode_model_year=False)
assert v.make == "Toyota"
assert v.model == "RAV4"
21 changes: 7 additions & 14 deletions tests/cars/test_honda.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


@parametrize_from_file
def test_honda(
def test_honda_2024(
vin: str,
model_year: int,
make: str,
Expand All @@ -12,21 +12,14 @@ def test_honda(
trim: str,
body_class: str,
electrification_level: str,
vehicle_type: str,
) -> None:
v = VIN(vin)
# assert f"{model_year} {make} {model}".rstrip().replace(" ", " ") == v.description
assert model_year == v.model_year
assert make.lower() == v.make.lower()
assert model == v.model

if series:
assert series == v.series

if trim:
assert trim == v.trim

if body_class:
assert body_class == v.body_class

if electrification_level:
assert electrification_level == v.electrification_level
assert body_class == v.body_class
assert series == v.series
assert trim == v.trim
assert vehicle_type.lower() == v.vehicle_type.lower()
assert electrification_level == v.electrification_level
Loading

0 comments on commit 6078875

Please sign in to comment.