Skip to content

Commit 6078875

Browse files
committed
v0.5.0
1 parent c58e4ab commit 6078875

File tree

11 files changed

+111295
-1515
lines changed

11 files changed

+111295
-1515
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.5.0 (2024-03-04)
4+
5+
[GitHub release](https://github.com/davidpeckham/vin/releases/tag/v0.5.0)
6+
7+
### New Features
8+
9+
* Decodes VINs that have an incorrect or zero model year character (partially resolves https://github.com/davidpeckham/vin/issues/2, sshane).
10+
* More unit tests
11+
312
## v0.4.3 (2024-02-25)
413

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

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.3"
4+
__version__ = "0.5.0"

src/vin/__init__.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5-
"""A Vehicle Identification Number (VIN).
6-
7-
"""
5+
"""A Vehicle Identification Number (VIN)."""
86

97
# ruff: noqa: TRY003, EM101, EM102
108

@@ -74,14 +72,22 @@ class VIN:
7472
7573
"""
7674

77-
def __init__(self, vin: str, decode: bool = True, fix_check_digit: bool = False) -> None:
75+
def __init__(
76+
self,
77+
vin: str,
78+
decode: bool = True,
79+
fix_check_digit: bool = False,
80+
decode_model_year: bool = True,
81+
) -> None:
7882
"""Validates the VIN and decodes vehicle information.
7983
8084
Args:
8185
vin: The 17-digit Vehicle Identification Number.
8286
decode: Decode vehicle details from the NHTSA vPIC database
83-
fix_check_digit: If True, fix an incorrect check digit
84-
instead of raising a ValueError.
87+
fix_check_digit: If True, fix an incorrect check digit instead of raising a ValueError.
88+
decode_model_year: If True, validate the model year character. If False, ignore the \
89+
model year character, which can be useful for vehicles manufactured for markets \
90+
outside North America (though results may be incomplete or inaccurate).
8591
8692
Raises:
8793
TypeError: `vin` is not a string.
@@ -94,7 +100,7 @@ def __init__(self, vin: str, decode: bool = True, fix_check_digit: bool = False)
94100
raise TypeError("VIN must be a string")
95101
if len(vin) != VIN_LENGTH:
96102
raise ValueError(f"VIN must be exactly {VIN_LENGTH} characters long")
97-
if vin[9] not in VIN_MODEL_YEAR_CHARACTERS:
103+
if decode_model_year and vin[9] not in VIN_MODEL_YEAR_CHARACTERS:
98104
raise ValueError(
99105
"VIN model year character must be one of these characters "
100106
f"{VIN_MODEL_YEAR_CHARACTERS}"
@@ -111,7 +117,7 @@ def __init__(self, vin: str, decode: bool = True, fix_check_digit: bool = False)
111117

112118
self._vin: str = vin
113119
if decode:
114-
self._decode_vin()
120+
self._decode_vin(decode_model_year)
115121
return
116122

117123
@property
@@ -527,7 +533,7 @@ def _decode_model_year(self) -> int:
527533
-2025
528534
"""
529535
year_code = self._vin[9]
530-
assert year_code in VIN_MODEL_YEAR_CHARACTERS
536+
# assert year_code in VIN_MODEL_YEAR_CHARACTERS
531537
model_year = 0
532538
conclusive = False
533539

@@ -558,7 +564,7 @@ def _decode_model_year(self) -> int:
558564

559565
return model_year if conclusive else -model_year
560566

561-
def _decode_vin(self) -> None:
567+
def _decode_vin(self, decode_model_year=True) -> None:
562568
"""decode the VIN to get manufacturer, make, model, and other vehicle details
563569
564570
Args:
@@ -567,13 +573,17 @@ def _decode_vin(self) -> None:
567573
Raises:
568574
DecodingError: Unable to decode VIN using NHTSA vPIC.
569575
"""
570-
model_year = self._decode_model_year()
571-
if model_year > 0:
572-
vehicle = decode_vin(self.wmi, self.descriptor, model_year)
576+
if decode_model_year:
577+
model_year = self._decode_model_year()
578+
if model_year > 0:
579+
vehicle = decode_vin(self.wmi, self.descriptor, model_year)
580+
else:
581+
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year))
582+
if not vehicle:
583+
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year) - 30)
573584
else:
574-
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year))
575-
if not vehicle:
576-
vehicle = decode_vin(self.wmi, self.descriptor, abs(model_year) - 30)
585+
vehicle = decode_vin(self.wmi, self.descriptor)
586+
577587
if vehicle is None:
578588
raise DecodingError()
579589

src/vin/database.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,24 @@ def get_wmis_for_cars_and_light_trucks() -> list[str]:
5555
"""
5656

5757

58-
def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
58+
def decode_vin(wmi: str, vds: str, model_year: int | None = None) -> dict | None:
5959
"""get vehicle details
6060
6161
Args:
6262
vin: The 17-digit Vehicle Identification Number.
63+
model_year: The vehicle model year. Outside North America, the VIN model year
64+
character may always be set to zero. When model_year is None, we will try
65+
to decode the VIN, but the information it returns may not be accurate.
6366
6467
Returns:
6568
Vehicle: the vehicle details
6669
"""
67-
if results := query(sql=DECODE_VIN_SQL, args=(wmi, model_year, vds)):
70+
if model_year is not None:
71+
results = query(sql=DECODE_VIN_SQL, args=(wmi, model_year, vds))
72+
else:
73+
results = query(sql=DECODE_VIN_WITHOUT_MODEL_YEAR_SQL, args=(wmi, vds))
74+
75+
if results:
6876
details: dict[str, Any] = {"model_year": model_year}
6977
for row in results:
7078
details.update(
@@ -89,6 +97,7 @@ def decode_vin(wmi: str, vds: str, model_year: int) -> dict | None:
8997
if make := get_make_from_wmi(wmi):
9098
details["make"] = make
9199
return details
100+
92101
return None
93102

94103

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

143+
DECODE_VIN_WITHOUT_MODEL_YEAR_SQL = """
144+
select
145+
pattern.id,
146+
pattern.vds,
147+
manufacturer.name as manufacturer,
148+
make.name as make,
149+
model.name as model,
150+
series.name as series,
151+
trim.name as trim,
152+
vehicle_type.name as vehicle_type,
153+
truck_type.name as truck_type,
154+
country.name as country,
155+
body_class.name as body_class,
156+
electrification_level.name as electrification_level
157+
from
158+
pattern
159+
join manufacturer on manufacturer.id = pattern.manufacturer_id
160+
join wmi on wmi.code = pattern.wmi
161+
join vehicle_type on vehicle_type.id = wmi.vehicle_type_id
162+
left join truck_type on truck_type.id = wmi.truck_type_id
163+
left join country on country.alpha_2_code = wmi.country
164+
left join make_model on make_model.model_id = pattern.model_id
165+
left join make on make.id = make_model.make_id
166+
left join model on model.id = pattern.model_id
167+
left join series on series.id = pattern.series_id
168+
left join trim on trim.id = pattern.trim_id
169+
left join body_class on body_class.id = pattern.body_class_id
170+
left join electrification_level on electrification_level.id = pattern.electrification_level_id
171+
where
172+
pattern.wmi = ?
173+
and REGEXP (?, pattern.vds)
174+
order by
175+
pattern.from_year desc,
176+
coalesce(pattern.updated, pattern.created) desc,
177+
pattern.id asc;
178+
"""
179+
"""Sort order is important. Best match and most recent patterns on top."""
180+
134181

135182
def get_make_from_wmi(wmi: str) -> str:
136183
"""Get the name of the make produced by a WMI. Used when the VIN is wrong or incomplete.

tests/cars/test_europe.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from vin import VIN
2+
3+
4+
def test_taiwan():
5+
v = VIN("JTHBYLFF305000302", fix_check_digit=True, decode_model_year=False)
6+
assert v.make == "Lexus"
7+
assert v.model == "LS"
8+
9+
10+
def test_italy():
11+
v = VIN("JTMW53FV60D023016", fix_check_digit=True, decode_model_year=False)
12+
assert v.make == "Toyota"
13+
assert v.model == "RAV4"

tests/cars/test_honda.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
@parametrize_from_file
6-
def test_honda(
6+
def test_honda_2024(
77
vin: str,
88
model_year: int,
99
make: str,
@@ -12,21 +12,14 @@ def test_honda(
1212
trim: str,
1313
body_class: str,
1414
electrification_level: str,
15+
vehicle_type: str,
1516
) -> None:
1617
v = VIN(vin)
17-
# assert f"{model_year} {make} {model}".rstrip().replace(" ", " ") == v.description
1818
assert model_year == v.model_year
1919
assert make.lower() == v.make.lower()
2020
assert model == v.model
21-
22-
if series:
23-
assert series == v.series
24-
25-
if trim:
26-
assert trim == v.trim
27-
28-
if body_class:
29-
assert body_class == v.body_class
30-
31-
if electrification_level:
32-
assert electrification_level == v.electrification_level
21+
assert body_class == v.body_class
22+
assert series == v.series
23+
assert trim == v.trim
24+
assert vehicle_type.lower() == v.vehicle_type.lower()
25+
assert electrification_level == v.electrification_level

0 commit comments

Comments
 (0)