├── .gitignore ├── src └── vininfo │ ├── dicts │ ├── __init__.py │ ├── regions.py │ ├── bodies.py │ ├── countries.py │ └── wmi.py │ ├── exceptions.py │ ├── __init__.py │ ├── assemblers.py │ ├── details │ ├── __init__.py │ ├── dafra.py │ ├── avtovaz.py │ ├── nissan.py │ ├── _base.py │ ├── renault.py │ ├── bajaj.py │ └── opel.py │ ├── brands.py │ ├── cli.py │ ├── utils.py │ ├── common.py │ └── toolbox.py ├── AUTHORS.md ├── INSTALL.md ├── CONTRIBUTING.md ├── tests ├── test_opel.py ├── test_nissan.py ├── test_renault.py ├── test_lada.py ├── test_module.py ├── test_dafra.py └── test_bajaj.py ├── .github └── workflows │ └── python-package.yml ├── ruff.toml ├── LICENSE ├── pyproject.toml ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | .idea 4 | .tox 5 | __pycache__ 6 | *.pyc 7 | *.pyo 8 | *.egg-info 9 | docs/_build/ 10 | 11 | -------------------------------------------------------------------------------- /src/vininfo/dicts/__init__.py: -------------------------------------------------------------------------------- 1 | from .countries import COUNTRIES 2 | from .regions import REGIONS 3 | from .wmi import WMI 4 | 5 | __all__ = ['COUNTRIES', 'REGIONS', 'WMI'] 6 | -------------------------------------------------------------------------------- /src/vininfo/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class VininfoException(Exception): 3 | """Base exception.""" 4 | 5 | 6 | class ValidationError(VininfoException): 7 | """Data validation exception.""" 8 | -------------------------------------------------------------------------------- /src/vininfo/dicts/regions.py: -------------------------------------------------------------------------------- 1 | 2 | REGIONS = { 3 | 'ABC': 'Africa', 4 | 'JKLMNPR': 'Asia', 5 | 'STUVWXYZ': 'Europe', 6 | '123457': 'North America', 7 | '6': 'Oceania', 8 | '89': 'South America', 9 | } 10 | -------------------------------------------------------------------------------- /src/vininfo/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import ValidationError, VininfoException 2 | from .toolbox import Vin 3 | 4 | __all__ = ['ValidationError', 'Vin', 'VininfoException'] 5 | 6 | VERSION = '1.9.2' 7 | """Application version number.""" -------------------------------------------------------------------------------- /src/vininfo/assemblers.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from .brands import Bajaj 4 | from .brands import Dafra as DafraBrand 5 | from .common import Assembler 6 | 7 | 8 | class Dafra(Assembler): 9 | brands: ClassVar = {DafraBrand(), Bajaj(), 'BMW'} 10 | -------------------------------------------------------------------------------- /src/vininfo/details/__init__.py: -------------------------------------------------------------------------------- 1 | from .avtovaz import AvtoVazDetails 2 | from .bajaj import BajajDetails 3 | from .dafra import DafraDetails 4 | from .nissan import NissanDetails 5 | from .opel import OpelDetails 6 | from .renault import RenaultDetails 7 | 8 | __all__ = [ 9 | 'AvtoVazDetails', 'BajajDetails', 'DafraDetails', 'NissanDetails', 'OpelDetails', 'RenaultDetails' 10 | ] 11 | 12 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # vininfo authors 2 | 3 | Created by Igor `idle sign` Starikov. 4 | 5 | 6 | ## Contributors 7 | 8 | * Jesse Liles 9 | * Stas Lipovenko 10 | * Ilya Andrushkevich 11 | * Ghiles Meddour 12 | * casual-citroen-enjoyer 13 | * Breno Ribeiro 14 | * Rodrigo Catto 15 | -------------------------------------------------------------------------------- /src/vininfo/brands.py: -------------------------------------------------------------------------------- 1 | from .common import Brand 2 | from .details import * 3 | 4 | 5 | class Lada(Brand): 6 | 7 | extractor = AvtoVazDetails 8 | 9 | 10 | class Nissan(Brand): 11 | 12 | extractor = NissanDetails 13 | 14 | 15 | class Opel(Brand): 16 | 17 | extractor = OpelDetails 18 | 19 | 20 | class Renault(Brand): 21 | 22 | extractor = RenaultDetails 23 | 24 | 25 | class Dafra(Brand): 26 | 27 | extractor = DafraDetails 28 | 29 | 30 | class Bajaj(Brand): 31 | 32 | extractor = BajajDetails -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # vininfo installation 2 | 3 | Python ``pip`` package is required to install ``vininfo``. 4 | 5 | 6 | ## From sources 7 | 8 | Use the following command line to install ``vininfo`` from sources directory (containing `pyprogram.toml`): 9 | 10 | ```bash 11 | pip install . 12 | ``` 13 | 14 | ## From PyPI 15 | 16 | Alternatively you can install ``vininfo`` from PyPI: 17 | 18 | ```bash 19 | pip install vininfo 20 | ``` 21 | 22 | Use `-U` flag for upgrade: 23 | 24 | ```bash 25 | pip install -U vininfo 26 | ``` 27 | -------------------------------------------------------------------------------- /src/vininfo/details/dafra.py: -------------------------------------------------------------------------------- 1 | from ..common import constant_info 2 | from ..dicts.bodies import BODY_MOTORCYCLE 3 | from ._base import Detail, VinDetails 4 | 5 | 6 | class DafraDetails(VinDetails): 7 | 8 | model = Detail(('vds', slice(0, 2)), { 9 | 'CA': 'Speed 150', 10 | 'CB': 'Kansas 150', 11 | }) 12 | 13 | # for a while, it will be a motorcycle regardless of the code source 14 | body = Detail(None, constant_info(BODY_MOTORCYCLE)) 15 | 16 | plant = Detail(('vis', 1), { 17 | 'M': 'Manaus', 18 | }) 19 | 20 | serial = Detail(('vis', slice(2, None))) -------------------------------------------------------------------------------- /src/vininfo/dicts/bodies.py: -------------------------------------------------------------------------------- 1 | 2 | BODY_CABRI_2 = 'Cabriolet, 2-Door' 3 | BODY_COUPE_2 = 'Coupe, 2-Door' 4 | BODY_CROSS_3 = 'Crossover, 3-Door' 5 | BODY_PICKUP_2 = 'Pickup, 2-Door' 6 | 7 | BODY_HATCH_3 = 'Hatchback, 3-Door' 8 | BODY_HATCH_5 = 'Hatchback, 5-Door' 9 | 10 | BODY_MINIVAN_3 = 'Minivan, 3-Door' 11 | BODY_MINIVAN_5 = 'Minivan, 5-Door' 12 | 13 | BODY_SEDAN_4 = 'Sedan, 4-Door' 14 | BODY_SEDAN_2 = 'Sedan, 2-Door' 15 | 16 | BODY_SW_3 = 'Station Wagon, 3-Door' 17 | BODY_SW_5 = 'Station Wagon, 5-Door' 18 | BODY_SW_8 = 'Station Wagon, 8-Door' 19 | 20 | BODY_MINIBUS = 'Minibus' 21 | BODY_VAN = 'Van' 22 | 23 | BODY_MOTORCYCLE = 'Motorcycle' -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # vininfo contributing 2 | 3 | ## Submit issues 4 | 5 | If you spotted something weird in application behavior or want to propose a feature you are welcome. 6 | 7 | 8 | ## Write code 9 | 10 | If you are eager to participate in application development and to work on an existing issue (whether it should 11 | be a bugfix or a feature implementation), fork, write code, and make a pull request right from the forked project page. 12 | 13 | 14 | ## Spread the word 15 | 16 | If you have some tips and tricks or any other words that you think might be of interest for the others — publish it 17 | wherever you find convenient. 18 | 19 | 20 | See also: https://github.com/idlesign/vininfo 21 | -------------------------------------------------------------------------------- /src/vininfo/details/avtovaz.py: -------------------------------------------------------------------------------- 1 | from ..dicts.bodies import * 2 | from ._base import Detail, VinDetails 3 | 4 | 5 | class AvtoVazDetails(VinDetails): 6 | """AvtoVAZ VIN details extractor.""" 7 | 8 | model = Detail(('vds', 1), { 9 | 'A': 'XRAY', 10 | 'F': 'Vesta', 11 | }) 12 | 13 | body = Detail(('vds', 2), { 14 | 'B': BODY_HATCH_5, 15 | 'K': BODY_SW_5, 16 | 'L': BODY_SEDAN_4, 17 | }) 18 | 19 | engine = Detail(('vds', 3), { 20 | '1': '21129', 21 | '2': '11189', 22 | '3': '21179', 23 | '4': 'H4M', 24 | 'A': '21129 CNG', 25 | }) 26 | 27 | plant = Detail(('vis', 1), { 28 | '0': 'Tolyatti', 29 | 'Y': 'Izhevsk', 30 | }) 31 | 32 | transmission = Detail(('vds', 4), { 33 | '1': 'Manual, 5-Gear (VAZ 21807)', 34 | '2': 'Semi-automatic, 5-Gear (VAZ 21827)', 35 | '3': 'Manual, 5-Gear (Renault JH3 514)', 36 | }) 37 | 38 | serial = Detail(('vis', slice(2, None))) 39 | -------------------------------------------------------------------------------- /tests/test_opel.py: -------------------------------------------------------------------------------- 1 | from vininfo import Vin 2 | 3 | 4 | def test_opel(): 5 | 6 | vin = Vin('W0LPC6DB3CC123456') 7 | 8 | assert f'{vin}' 9 | assert vin.wmi == 'W0L' 10 | assert vin.manufacturer == 'Opel/Vauxhall' 11 | assert vin.vds == 'PC6DB3' 12 | assert vin.vis == 'CC123456' 13 | assert vin.years_code == 'C' 14 | assert vin.years == [2012, 1982] 15 | assert vin.region_code == 'W' 16 | assert vin.region == 'Europe' 17 | assert vin.country_code == 'W0' 18 | assert vin.country == 'Germany' 19 | assert f'{vin.brand}' == 'Opel (Opel/Vauxhall)' 20 | 21 | details = vin.details 22 | assert details.model.code == 'P' 23 | assert details.model.name == ['Astra J', 'Zafira C'] 24 | assert details.body.code == '6' 25 | assert details.body.name == 'Hatchback, 5-Door' 26 | assert details.engine.code == 'B' 27 | assert details.engine.name == 'A14XER100HP' 28 | assert details.plant.code == 'C' 29 | assert details.plant.name == 'Yelabuga' 30 | assert details.serial.code == '123456' 31 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: ["3.10", 3.11, 3.12, 3.13] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Setup uv 26 | uses: astral-sh/setup-uv@v6 27 | - name: Install deps 28 | run: | 29 | uv sync --only-group tests 30 | uv pip install coveralls 31 | - uses: astral-sh/ruff-action@v3 32 | with: 33 | version: 0.13.1 34 | args: check 35 | - name: Run tests 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.github_token }} 38 | run: | 39 | uv run coverage run -m pytest 40 | uv run coveralls --service=github 41 | -------------------------------------------------------------------------------- /tests/test_nissan.py: -------------------------------------------------------------------------------- 1 | from vininfo import Vin 2 | 3 | 4 | def test_nissan(): 5 | 6 | vin = Vin('5N1NJ01CXST000001') 7 | 8 | assert f'{vin}' 9 | assert vin.wmi == '5N1' 10 | assert vin.manufacturer == 'Nissan' 11 | assert vin.vds == 'NJ01CX' 12 | assert vin.vis == 'ST000001' 13 | assert vin.years_code == 'S' 14 | assert vin.years == [2025, 1995] 15 | assert vin.region_code == '5' 16 | assert vin.region == 'North America' 17 | assert vin.country_code == '5N' 18 | assert vin.country == 'United States' 19 | assert f'{vin.brand}' == 'Nissan (Nissan)' 20 | 21 | details = vin.details 22 | assert details.model.code == 'J' 23 | assert details.model.name == 'Maxima' 24 | assert details.body.code == '1' 25 | assert details.body.name == ['Sedan, 4-Door', 'Standard Body Truck'] 26 | assert details.engine.code == 'N' 27 | assert details.engine.name == 'VH45DE' 28 | assert details.plant.code == 'T' 29 | assert details.plant.name == ['Tochigi', 'Oppama'] 30 | assert details.serial.code == '000001' 31 | 32 | assert vin.verify_checksum() 33 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py310" 2 | line-length = 120 3 | 4 | [format] 5 | quote-style = "single" 6 | exclude = [] 7 | 8 | [lint] 9 | select = [ 10 | "B", # possible bugs 11 | "BLE", # broad exception 12 | "C4", # comprehensions 13 | "DTZ", # work with datetimes 14 | "E", # code style 15 | "ERA", # commented code 16 | "EXE", # check executables 17 | "F", # misc 18 | "FA", # future annotations 19 | "FBT", # booleans 20 | "FURB", # modernizing 21 | "G", # logging format 22 | "I", # imports 23 | "ICN", # import conventions 24 | "INT", # i18n 25 | "ISC", # stringc concat 26 | "PERF", # perfomance 27 | "PIE", # misc 28 | "PLC", # misc 29 | "PLE", # misc err 30 | "PT", # pytest 31 | "PTH", # pathlib 32 | "PYI", # typing 33 | "RSE", # exc raise 34 | "RUF", # misc 35 | "SLOT", # slots related 36 | "TC", # typing 37 | "UP", # py upgrade 38 | ] 39 | 40 | ignore = [ 41 | "F403", 42 | "F405", 43 | ] 44 | 45 | 46 | [lint.extend-per-file-ignores] 47 | "src/vininfo/utils.py" = [ 48 | "PLC0415", 49 | ] 50 | "tests/test_module.py" = [ 51 | "PLC0415", 52 | ] 53 | -------------------------------------------------------------------------------- /src/vininfo/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | from vininfo import VERSION, Vin 6 | 7 | 8 | @click.group() 9 | @click.version_option(version=VERSION) 10 | def entry_point(): 11 | """vininfo command line utilities.""" 12 | 13 | 14 | @entry_point.command() 15 | @click.argument('vin') 16 | def show(vin): 17 | """Show information for VIN""" 18 | num = Vin(vin) 19 | 20 | click.secho('Basic:') 21 | 22 | def out(annotatable): 23 | for k, v in annotatable.annotate().items(): 24 | click.secho(f'{k}: ', fg='green', nl=False) 25 | click.secho(v) 26 | 27 | out(num) 28 | 29 | details = num.details 30 | 31 | if details: 32 | click.secho('') 33 | click.secho('Details:') 34 | out(details) 35 | 36 | 37 | @entry_point.command() 38 | @click.argument('vin') 39 | def check(vin): 40 | """Perform VIN checksum validation""" 41 | 42 | if Vin(vin).verify_checksum(): 43 | click.secho('Checksum is valid', fg='green') 44 | else: 45 | click.secho('Checksum is not valid', fg='red', err=True) 46 | sys.exit(1) 47 | 48 | 49 | def main(): 50 | entry_point(obj={}) 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /tests/test_renault.py: -------------------------------------------------------------------------------- 1 | from vininfo import Vin 2 | 3 | 4 | def test_renault(): 5 | 6 | vin = Vin('VF14SRAP451234567') 7 | 8 | assert f'{vin}' 9 | assert vin.wmi == 'VF1' 10 | assert vin.manufacturer == 'Renault' 11 | assert vin.vds == '4SRAP4' 12 | assert vin.vis == '51234567' 13 | assert vin.years_code == '5' 14 | assert vin.years == [2005] 15 | assert vin.region_code == 'V' 16 | assert vin.region == 'Europe' 17 | assert vin.country_code == 'VF' 18 | assert vin.country == 'France' 19 | assert f'{vin.brand}' == 'Renault (Renault)' 20 | 21 | details = vin.details 22 | assert not details.engine 23 | assert details.model 24 | assert details.model.code == 'S' 25 | assert details.model.name == ['Logan', 'Sandero', 'Duster', 'Dokker', 'Lodgy'] 26 | assert details.body.code == '4' 27 | assert details.body.name == 'Sedan, 4-Door' 28 | assert details.plant.code == 'P' 29 | assert details.plant.name == 'Mexico' 30 | assert details.transmission.code == '4' 31 | assert details.transmission.name == 'Manual, 5-Gears' 32 | assert details.serial.code == '1234567' 33 | 34 | 35 | def test_bogus(): 36 | vin = Vin('VF1KG1PBE34488860') 37 | details = vin.details 38 | assert details.engine.code == '' 39 | assert not details.engine 40 | -------------------------------------------------------------------------------- /src/vininfo/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def merge_wmi(new_wmis: dict) -> tuple[set, str]: 4 | """Helper. Used to update vininfo.dicts.wmi.WMI dictionary 5 | with entries from another dictionary. 6 | 7 | Existing keys are skipped, new keys are added, result is sorted. 8 | 9 | Returns a two-items tuple: 10 | 0. set of new keys borrowed from `new_wmis`; 11 | 1. source code string to replace the current `WMI` dictionary with. 12 | 13 | :param new_wmis: Mapping from WMI (two or three chars) to manufacturer title. 14 | 15 | """ 16 | import inspect 17 | import re 18 | 19 | from .dicts import wmi 20 | 21 | wmi_src = re.search('WMI = {([^}]+)}', inspect.getsource(wmi), re.MULTILINE) 22 | 23 | assert wmi_src, 'Unable to parse WMI dict body' 24 | 25 | wmi_src_dict = {} 26 | 27 | for line in wmi_src.group(1).splitlines(): 28 | line = line.strip(', ') 29 | 30 | if not line: 31 | continue 32 | 33 | key, _, value = line.partition(':') 34 | wmi_src_dict[key.strip('\'"')] = value.strip() 35 | 36 | wmi_missing = set(new_wmis.keys()).difference(wmi_src_dict.keys()) 37 | 38 | for key in wmi_missing: 39 | wmi_src_dict[key] = f"'{new_wmis[key]}'" 40 | 41 | wmi_dst = ['WMI = {'] 42 | 43 | for key, value in sorted(wmi_src_dict.items(), key=lambda item: item[0]): 44 | wmi_dst.append(f" '{key}': {value},") 45 | 46 | wmi_dst.append('}') 47 | 48 | return wmi_missing, '\n'.join(wmi_dst) 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2025, Igor `idle sign` Starikov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the vininfo nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /tests/test_lada.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from vininfo import Vin 4 | 5 | 6 | def test_lada(): 7 | 8 | vin = Vin('XTAGFK330JY144213') 9 | 10 | assert f'{vin}' 11 | assert vin.wmi == 'XTA' 12 | assert vin.manufacturer == 'AvtoVAZ' 13 | assert vin.vds == 'GFK330' 14 | assert vin.vis == 'JY144213' 15 | assert vin.years_code == 'J' 16 | assert vin.years == [2018, 1988] 17 | assert vin.region_code == 'X' 18 | assert vin.region == 'Europe' 19 | assert vin.country_code == 'XT' 20 | assert vin.country == 'Russia' 21 | assert vin.annotate() == OrderedDict([ 22 | ('Country', 'Russia'), 23 | ('Manufacturer', 'AvtoVAZ'), 24 | ('Region', 'Europe'), 25 | ('Years', '2018, 1988'), 26 | ]) 27 | assert f'{vin.brand}' == 'Lada (AvtoVAZ)' 28 | 29 | details = vin.details 30 | assert details.model.code == 'F' 31 | assert details.model.name == 'Vesta' 32 | assert details.body.code == 'K' 33 | assert details.body.name == 'Station Wagon, 5-Door' 34 | assert details.engine.code == '3' 35 | assert details.engine.name == '21179' 36 | assert details.transmission.code == '3' 37 | assert details.transmission.name == 'Manual, 5-Gear (Renault JH3 514)' 38 | assert details.plant.code == 'Y' 39 | assert details.plant.name == 'Izhevsk' 40 | assert details.annotate() == OrderedDict([ 41 | ('Body', 'Station Wagon, 5-Door'), 42 | ('Engine', '21179'), 43 | ('Model', 'Vesta'), 44 | ('Plant', 'Izhevsk'), 45 | ('Serial', '144213'), 46 | ('Transmission', 'Manual, 5-Gear (Renault JH3 514)'), 47 | ]) 48 | assert not vin.verify_checksum() 49 | -------------------------------------------------------------------------------- /src/vininfo/details/nissan.py: -------------------------------------------------------------------------------- 1 | from ..dicts.bodies import * 2 | from ._base import Detail, VinDetails 3 | 4 | 5 | class NissanDetails(VinDetails): 6 | """Nissan VIN details extractor.""" 7 | 8 | model = Detail(('vds', 1), { 9 | 'A': ['Armada', 'Titan', 'Maxima'], 10 | 'B': 'Sentra', 11 | 'C': 'Versa (07-11)', 12 | 'D': ['Truck', 'Xterra (00-04)', 'Frontier'], 13 | 'J': 'Maxima', 14 | 'N': 'Xterra (05-11)', 15 | 'R': 'Pathfinder', 16 | 'S': ['240SX', 'Rogue (08-11)'], 17 | 'U': 'Altima', 18 | 'Z': ['300Z', '350Z', 'Murano'], 19 | }) 20 | 21 | body = Detail(('vds', 3), { 22 | '1': [BODY_SEDAN_4, 'Standard Body Truck'], 23 | '4': BODY_COUPE_2, 24 | '5': BODY_SW_5, 25 | '6': [BODY_CABRI_2, 'Fastback', 'King Cab Truck'], 26 | '7': 'Crew Cab Truck', 27 | '8': BODY_SW_8, 28 | }) 29 | 30 | engine = Detail(('vds', 0), { 31 | 'A': ['VG30D', 'VK45DE', 'VQ35DE', 'VK56DE', 'VQ40DE', 'QR25DE'], 32 | 'B': ['KA24DE', 'SR20DE', 'VQ35HR', 'MR18DE', 'QR25DE'], 33 | 'C': ['SR20DE', 'VG30DETT', 'QG18DE'], 34 | 'D': ['KA24DE', 'QG18DE', 'VQ35DE'], 35 | 'E': ['VE30DE', 'GA16DE', 'VG33E'], 36 | 'F': 'KA24E', 37 | 'H': 'VG30E', 38 | 'M': ['KA24DE', 'VG33ER'], 39 | 'N': 'VH45DE', 40 | 'R': 'VG30DE', 41 | 'S': 'KA24E', 42 | 'T': 'VG33E', 43 | }) 44 | 45 | plant = Detail(('vis', 1), { 46 | 'C': 'Smyrna', 47 | 'L': 'Aguas Calientes', 48 | 'M': 'Tochigi', 49 | 'N': 'Canton', 50 | 'T': ['Tochigi', 'Oppama'], 51 | 'W': 'Kyushyu', 52 | }) 53 | 54 | serial = Detail(('vis', slice(2, None))) 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "vininfo" 3 | dynamic = ["version"] 4 | description = "Extracts useful information from Vehicle Identification Number (VIN)" 5 | authors = [ 6 | { name = "Igor Starikov", email = "idlesign@yandex.ru" } 7 | ] 8 | readme = "README.md" 9 | license = "BSD-3-Clause" 10 | license-files = ["LICENSE"] 11 | requires-python = ">=3.10" 12 | keywords = ["vin", "vehicles"] 13 | dependencies = [] 14 | 15 | [project.urls] 16 | Homepage = "https://github.com/idlesign/vininfo" 17 | 18 | [project.scripts] 19 | vininfo = "vininfo.cli:main" 20 | 21 | [dependency-groups] 22 | dev = [ 23 | {include-group = "docs"}, 24 | {include-group = "linters"}, 25 | {include-group = "tests"}, 26 | ] 27 | docs = [ 28 | ] 29 | linters = [ 30 | "ruff==0.13.1", 31 | ] 32 | tests = [ 33 | "pytest", 34 | ] 35 | 36 | [project.optional-dependencies] 37 | cli = [ 38 | "click", 39 | ] 40 | 41 | [build-system] 42 | requires = ["hatchling"] 43 | build-backend = "hatchling.build" 44 | 45 | [tool.hatch.version] 46 | path = "src/vininfo/__init__.py" 47 | 48 | [tool.hatch.build.targets.wheel] 49 | packages = ["src/vininfo"] 50 | 51 | [tool.hatch.build.targets.sdist] 52 | packages = ["src/"] 53 | 54 | [tool.pytest.ini_options] 55 | testpaths = [ 56 | "tests", 57 | ] 58 | 59 | [tool.coverage.run] 60 | source = [ 61 | "src/", 62 | ] 63 | omit = [ 64 | ] 65 | 66 | [tool.coverage.report] 67 | fail_under = 90.00 68 | exclude_also = [ 69 | "raise NotImplementedError", 70 | "if TYPE_CHECKING:", 71 | ] 72 | 73 | [tool.tox] 74 | skip_missing_interpreters = true 75 | env_list = [ 76 | "py310", 77 | "py311", 78 | "py312", 79 | "py313", 80 | ] 81 | 82 | [tool.tox.env_run_base] 83 | dependency_groups = ["tests"] 84 | commands = [ 85 | ["pytest", { replace = "posargs", default = ["tests"], extend = true }], 86 | ] 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vininfo changelog 2 | 3 | 4 | ### v1.9.2 [2025-09-20] 5 | * ** Fixed a regression of Vin.years for unsupported year codes (closes #32). 6 | 7 | ### v1.9.1 [2025-06-13] 8 | * ** Fix brand resolution bug introduced with Assembler support. 9 | 10 | ### v1.9.0 [2025-06-09] 11 | * ++ Added Assembler info. 12 | * ++ Countries database update. 13 | 14 | ### v1.8.0 [2024-01-30] 15 | 16 | + Add Serbia to countries. 17 | + Include Jeep Egypt codes. 18 | * Add QA for Python 3.11. 19 | 20 | 21 | ### v1.7.0 [2021-12-25] 22 | 23 | ! Regions and Countries list is updated according to ISO 3780. Info for vehicles made before 2009 may be affected. 24 | + Model year check in Vin.verify_checksum() is now optional, on by default (see #19). 25 | 26 | 27 | ### v1.6.0 [2021-08-30] 28 | 29 | + WMI dict is updated. 30 | 31 | 32 | ### v1.5.1 [2021-07-14] 33 | 34 | * Now using regex VIN validation. 35 | 36 | 37 | ### v1.5.0 [2021-02-04] 38 | 39 | + WMI database extended (see #8). 40 | 41 | 42 | ### v1.4.2 [2021-01-26] 43 | 44 | * Added Kia WMI. 45 | 46 | 47 | ### v1.4.1 [2020-07-30] 48 | 49 | * Fixed TypeError with some VINs (closes #6). 50 | 51 | 52 | ### v1.4.0 [2020-05-02] 53 | 54 | ! Dropped support for Py 2. 55 | * Added QA for Py 3.8. 56 | * Dropped QA for Py 3.5. 57 | 58 | 59 | ### v1.3.0 60 | 61 | + WMI database extended. 62 | 63 | 64 | ### v1.2.0 65 | 66 | + Added Vin.manufacturer_is_small property. 67 | + WMI database extended. 68 | 69 | 70 | ### v1.1.0 71 | 72 | + Allow extraction of basic information even for unsupported brands. 73 | 74 | 75 | ### v1.0.0 76 | 77 | ! Dropped QA for Python 2. 78 | ! Dropped QA for Python 3.4. 79 | * AvtoVAZ transmission types made more specific. 80 | * Celebrating 1.0.0. 81 | 82 | 83 | ### v0.3.0 84 | 85 | + Added basic info for Opel and Renault. 86 | + Details extractors definitions simplified. 87 | 88 | 89 | ### v0.2.0 90 | 91 | + Added basic info for Nissan. 92 | * Coachwork renamed to body. 93 | 94 | 95 | ### v0.1.0 96 | 97 | + Basic functionality. -------------------------------------------------------------------------------- /src/vininfo/details/_base.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, ClassVar 2 | 3 | from ..common import Annotatable 4 | 5 | if TYPE_CHECKING: # pragma: nocover 6 | from ..toolbox import Vin 7 | 8 | 9 | class DetailWrapper: 10 | 11 | __slots__ = ['_supported', 'code', 'name'] 12 | 13 | def __init__(self, details: 'VinDetails', detail: 'Detail'): 14 | """ 15 | :param details: 16 | :param detail: 17 | 18 | """ 19 | vin = details._vin 20 | 21 | source = detail.source 22 | 23 | code = '' 24 | 25 | if source: 26 | attr_name, attr_idx = source 27 | code_source = getattr(vin, attr_name) 28 | 29 | code = code_source[attr_idx] 30 | 31 | defs = detail.defs 32 | 33 | if callable(defs): 34 | defs = defs(details) 35 | 36 | self.code: str = code 37 | self.name: str | None = defs.get(code) 38 | 39 | self._supported = bool(source) or bool(self.name) 40 | """Flag indicating that this detail extraction is available.""" 41 | 42 | def __str__(self): 43 | return self.name or self.code 44 | 45 | def __bool__(self): 46 | return self._supported 47 | 48 | 49 | class Detail: 50 | """Vin detail descriptor.""" 51 | 52 | __slots__ = ['defs', 'source'] 53 | 54 | def __init__(self, code_source=None, definitions=None): 55 | self.source = code_source 56 | self.defs = definitions or {} 57 | 58 | def __get__(self, instance: 'VinDetails', owner: type['VinDetails']) -> DetailWrapper: 59 | """ 60 | :param instance: 61 | :param owner: 62 | 63 | """ 64 | return DetailWrapper(instance, self) 65 | 66 | 67 | class VinDetails(Annotatable): 68 | """Offers advanced (manufacturer specific) VIN data extraction ficilities.""" 69 | 70 | annotate_titles: ClassVar = { 71 | 'body': 'Body', 72 | 'engine': 'Engine', 73 | 'model': 'Model', 74 | 'plant': 'Plant', 75 | 'transmission': 'Transmission', 76 | 'serial': 'Serial', 77 | } 78 | 79 | def __init__(self, vin: 'Vin'): 80 | self._vin = vin 81 | 82 | body = Detail() 83 | engine = Detail() 84 | model = Detail() 85 | plant = Detail() 86 | transmission = Detail() 87 | serial = Detail() 88 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | import pytest 4 | 5 | from vininfo import ValidationError, Vin 6 | from vininfo.common import Annotatable 7 | 8 | 9 | def test_validation(): 10 | 11 | with pytest.raises(ValidationError): 12 | Vin('tooshort') 13 | 14 | with pytest.raises(ValidationError): 15 | Vin('AAAAAAAAAAAAAAAAO') 16 | 17 | with pytest.raises(ValidationError): 18 | Vin('AAAAAAAAAAAAAAAAO') 19 | 20 | with pytest.raises(ValidationError): 21 | Vin('1\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n1') 22 | 23 | with pytest.raises(ValidationError): 24 | Vin('AAAAAAAIAAAAAAAAA') 25 | 26 | with pytest.raises(ValidationError): 27 | Vin('AAAAAAA:AAAAAAAAA') 28 | 29 | 30 | def test_basic(): 31 | 32 | # number faked 33 | vin = Vin('JSA12345678901234') 34 | assert vin.manufacturer == 'Suzuki' 35 | assert not vin.manufacturer_is_small 36 | 37 | # number faked 38 | assert Vin('TM912345678901234').manufacturer_is_small 39 | 40 | 41 | def test_checksum(): 42 | 43 | assert Vin('1M8GDM9AXKP042788').verify_checksum() 44 | 45 | # faked 46 | assert not Vin('1M8GDM9AyKP042788').verify_checksum() 47 | 48 | # non strict 49 | non_strict = Vin('WBA71DC010CH14720') 50 | assert non_strict.verify_checksum(check_year=False) 51 | assert not non_strict.verify_checksum() 52 | 53 | 54 | def test_annotatable(): 55 | class NoAttr(Annotatable): 56 | annotate_titles: ClassVar = { 57 | 'no_attr': 'NoAttr' 58 | } 59 | no_attr = NoAttr() 60 | assert no_attr.annotate() == {} 61 | 62 | 63 | def test_unsupported_brand(): 64 | 65 | vin = Vin('200BL8EV9AX604020') 66 | assert vin.manufacturer == 'UnsupportedBrand' 67 | assert vin.country is None 68 | 69 | def test_unsupported_brand_knowing_assembler(): 70 | 71 | vin = Vin('95VBL8EV9AX604020') 72 | assert vin.manufacturer == 'Dafra' 73 | assert vin.brand.manufacturer == 'UnsupportedBrand' 74 | assert vin.country == 'Brazil' 75 | 76 | 77 | def test_merge_wmi(): 78 | from vininfo.utils import merge_wmi 79 | 80 | missing, lines = merge_wmi({'1DTEST': 'Some', '1GTEST': 'Other'}) 81 | assert missing == {'1DTEST', '1GTEST'} 82 | assert " '1D': 'Dodge',\n '1DTEST': 'Some'," in lines 83 | assert " '1GT': 'GMC Truck',\n '1GTEST': 'Other'," in lines 84 | 85 | 86 | def test_squish_vin(): 87 | assert Vin('KF1SF08WJ8B257338').squish_vin == 'KF1SF08W8B' 88 | 89 | 90 | def test_year_code_unsupported(): 91 | assert Vin('WBY21CF090CU47924').years == [] 92 | -------------------------------------------------------------------------------- /src/vininfo/details/renault.py: -------------------------------------------------------------------------------- 1 | from ..dicts.bodies import * 2 | from ._base import Detail, VinDetails 3 | 4 | 5 | class RenaultDetails(VinDetails): 6 | """Renault VIN details extractor.""" 7 | 8 | model = Detail(('vds', 1), { 9 | '0': 'Twingo', 10 | '1': 'R4', 11 | '2': 'R25', 12 | '3': 'R4', 13 | '4': ['R21', 'Express'], 14 | '5': ['Clio I', 'Laguna', 'R19', 'Safrane'], 15 | 'A': ['Megane I', 'Master'], 16 | 'B': 'Clio II', 17 | 'C': 'Kangoo', 18 | 'D': 'Master', 19 | 'E': ['Espace III', 'Avantime'], 20 | 'G': 'Laguna II', 21 | 'H': 'Master Propulsion', 22 | 'J': ['Vel Satis', 'New Trafic'], 23 | 'K': 'Espace IV', 24 | 'L': 'Trafic', 25 | 'M': 'Megan II', 26 | 'P': 'Modus', 27 | 'S': ['Logan', 'Sandero', 'Duster', 'Dokker', 'Lodgy'], 28 | 'Y': 'Koleos', 29 | }) 30 | 31 | body = Detail(('vds', 0), { 32 | '2': BODY_SEDAN_2, 33 | '3': BODY_HATCH_3, 34 | '4': BODY_SEDAN_4, 35 | '5': BODY_HATCH_5, 36 | '6': BODY_SW_5, 37 | '7': BODY_CABRI_2, 38 | '8': BODY_COUPE_2, 39 | 'A': BODY_SW_3, 40 | 'B': BODY_HATCH_5, 41 | 'C': BODY_HATCH_3, 42 | 'D': BODY_COUPE_2, 43 | 'E': BODY_CABRI_2, 44 | 'F': BODY_VAN, 45 | 'G': BODY_MINIVAN_3, 46 | 'J': BODY_MINIVAN_5, 47 | 'H': BODY_PICKUP_2, 48 | 'K': BODY_SW_3, 49 | 'L': BODY_SEDAN_4, 50 | 'M': BODY_SEDAN_2, 51 | 'N': BODY_MINIVAN_5, 52 | 'S': BODY_SW_5, 53 | 'U': BODY_PICKUP_2, 54 | }) 55 | 56 | plant = Detail(('vds', 4), { 57 | 'A': 'Portugal', 58 | 'B': 'Batilly', 59 | 'C': 'Creil', 60 | 'D': 'Douai', 61 | 'E': 'Spain', # ? city 62 | 'V': 'Spain', # ? city 63 | 'F': 'Flin', 64 | 'G': 'Grand Coronne / Novo Mesto', 65 | 'H': 'Haren', 66 | 'J': 'Billancourt', 67 | 'K': 'Dieppe', 68 | 'N': 'Mexico', 69 | 'P': 'Mexico', 70 | 'R': 'Bursa / Moscow', 71 | 'S': 'Sandouville', 72 | 'T': 'Romorantin', 73 | 'U': 'Maubeuge', 74 | 'W': 'Valladolid', 75 | 'X': 'Heuliez', 76 | 'Z': 'USA', # ? city 77 | }) 78 | 79 | transmission = Detail(('vds', 5), { 80 | '1': 'Automatic, 3-Gears', 81 | '2': 'Automatic, 4-Gears', 82 | '4': 'Manual, 5-Gears', 83 | '5': 'Manual, 5-Gears', 84 | '8': 'Manual, 5-Gears, 4x4', 85 | 'C': 'Manual, 5-Gears', 86 | 'D': 'Manual, 5-Gears', 87 | }) 88 | 89 | serial = Detail(('vis', slice(1, None))) 90 | -------------------------------------------------------------------------------- /src/vininfo/details/bajaj.py: -------------------------------------------------------------------------------- 1 | from ..common import candidate_by_year_model_mapping, constant_info 2 | from ..dicts.bodies import BODY_MOTORCYCLE 3 | from ._base import Detail, VinDetails 4 | 5 | 6 | def get_wmi(details: VinDetails): 7 | # noinspection PyProtectedMember 8 | return details._vin.wmi 9 | 10 | 11 | def get_years(details: VinDetails): 12 | # noinspection PyProtectedMember 13 | return details._vin.years 14 | 15 | 16 | def get_model(details: VinDetails): 17 | """It looks like Bajaj Brazil is using the same VDS mapping that Bajaj India, or a pretty close one""" 18 | 19 | # Model name pattern with spaces 20 | bajaj_commons_mapping = { 21 | 'A92': 'Dominar 160', 22 | 'A36': 'Dominar 200', 23 | 'A67': 'Dominar 400', 24 | 'B65': 'Dominar 250', 25 | } 26 | 27 | bajaj_brazil_mapping_by_year_model = { 28 | '2026-': { 29 | 'A92': 'Dominar NS160', 30 | 'A36': 'Dominar NS200', 31 | }, 32 | '-': { 33 | 'C41': 'Pulsar N150' 34 | } 35 | } 36 | 37 | bajaj_brazil_mapping = bajaj_commons_mapping 38 | wmi = get_wmi(details) 39 | if wmi == '92T': 40 | bajaj_brazil_mapping = bajaj_commons_mapping.copy() 41 | updates = candidate_by_year_model_mapping(bajaj_brazil_mapping_by_year_model, get_years(details)) 42 | if updates: 43 | bajaj_brazil_mapping.update(updates) 44 | 45 | candidates = { 46 | '92T': bajaj_brazil_mapping, 47 | '95V': { # Bajaj assembled by Dafra 48 | '2A1': 'Dominar 160', 49 | '2B1': 'Dominar 200', 50 | '3B1': 'Dominar 400', 51 | }, 52 | 'MD2': { 53 | **bajaj_commons_mapping, 54 | # Bajaj India 55 | 'B54': 'Pulsar N160', 56 | 'B97': 'Pulsar N250', 57 | } 58 | } 59 | 60 | return candidates.get(wmi, {}) 61 | 62 | 63 | def get_plant(details): 64 | candidates = { 65 | 'MD2': { # Bajaj India 66 | 'C': 'Chakan', 67 | 'W': 'Waluj', # Guessing 68 | 'P': 'Pant Nagar' # Guessing 69 | } 70 | } 71 | 72 | return candidates.get(get_wmi(details), { 73 | # WMI = 95V or 92T 74 | 'M': 'Manaus' 75 | }) 76 | 77 | 78 | class BajajDetails(VinDetails): 79 | # for a while, it will be decoded by the first 3 characters of vds 80 | model = Detail(('vds', slice(0, 3)), get_model) 81 | 82 | # for a while, it will be a motorcycle regardless of the code source 83 | body = Detail(None, constant_info(BODY_MOTORCYCLE)) 84 | 85 | plant = Detail(('vis', 1), get_plant) 86 | 87 | serial = Detail(('vis', slice(2, None))) 88 | -------------------------------------------------------------------------------- /src/vininfo/common.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import TYPE_CHECKING, Any, ClassVar 3 | 4 | if TYPE_CHECKING: # pragma: nocover 5 | from .details._base import VinDetails 6 | 7 | 8 | def constant_info(info): 9 | """Emulate details logic to always return the same information.""" 10 | return lambda details: type("", (), {"get": (lambda code: info)}) 11 | 12 | 13 | def candidate_by_year_model_mapping(mapping: dict[str, dict[str, str]], years: list[int]): 14 | candidate_mapping = {} 15 | for model_year_range, candidates in mapping.items(): 16 | start_model_year, end_model_year = model_year_range.split('-') 17 | 18 | if not start_model_year and not end_model_year: 19 | candidate_mapping.update(candidates) 20 | continue 21 | 22 | start_model_year = int(start_model_year) 23 | end_model_year = ( 24 | int(end_model_year) 25 | if end_model_year else 26 | max(datetime.now(tz=timezone.utc).year, start_model_year) + 1 27 | ) 28 | 29 | filter_years = [year for year in years if start_model_year <= year <= end_model_year] 30 | if filter_years: 31 | candidate_mapping.update(candidates) 32 | 33 | return candidate_mapping 34 | 35 | 36 | class Annotatable: 37 | 38 | annotate_titles: ClassVar = {} 39 | 40 | def annotate(self) -> dict[str, Any]: 41 | 42 | annotations = {} 43 | no_attr = set() 44 | 45 | for attr_name, label in self.annotate_titles.items(): 46 | value = getattr(self, attr_name, no_attr) 47 | 48 | if value is no_attr: 49 | continue 50 | 51 | if isinstance(value, list): 52 | value = ', '.join(f'{val}' for val in value) 53 | 54 | annotations[label] = f'{value}' 55 | 56 | return dict(sorted(annotations.items(), key=lambda item: item[0])) 57 | 58 | 59 | class Assembler: 60 | """Assembler is a manufacturer that has a WMI and assemble vehicles for other brands using its own WMI.""" 61 | __slots__ = ['manufacturer'] 62 | 63 | brands: ClassVar[set['Brand']] = None 64 | 65 | def __init__(self, manufacturer: str | None = None): 66 | self.manufacturer = manufacturer or self.title 67 | 68 | @property 69 | def title(self) -> str: 70 | return self.__class__.__name__ 71 | 72 | def __str__(self): 73 | return f'{self.title} ({self.manufacturer})' 74 | 75 | 76 | class Brand(Assembler): 77 | extractor: type['VinDetails'] = None 78 | 79 | @property 80 | def brands(self) -> set['Brand']: 81 | return {self} 82 | 83 | 84 | class UnsupportedBrand(Brand): 85 | """Unsupported brand.""" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vininfo 2 | 3 | https://github.com/idlesign/vininfo 4 | 5 | 6 | [![PyPI - Version](https://img.shields.io/pypi/v/vininfo)](https://pypi.python.org/pypi/vininfo) 7 | [![License](https://img.shields.io/pypi/l/vininfo)](https://pypi.python.org/pypi/vininfo) 8 | [![Coverage](https://img.shields.io/coverallsCoverage/github/idlesign/vininfo)](https://coveralls.io/r/idlesign/vininfo) 9 | 10 | 11 | ## Description 12 | 13 | *Extracts useful information from Vehicle Identification Number (VIN)* 14 | 15 | * Can be used as a standalone console application (CLI). 16 | * One can also use import it as any other package in your Python code. 17 | * Gives basic and detailed info (is available) about VIN. 18 | * Allows VIN checksum verification. 19 | 20 | Additional info available for many vehicles from: 21 | 22 | * AvtoVAZ 23 | * Nissan 24 | * Opel 25 | * Renault 26 | 27 | 28 | ## Requirements 29 | 30 | * Python 3.10+ 31 | * `click` package for CLI 32 | 33 | 34 | ## Usage 35 | 36 | ### CLI 37 | 38 | `click` package is required for CLI. You can install `vininfo` with `click` using: 39 | ```bash 40 | pip install vininfo[cli] 41 | ``` 42 | 43 | ```bash 44 | $ vininfo --help 45 | 46 | ; Print out VIN info: 47 | $ vininfo show XTAGFK330JY144213 48 | 49 | ; Basic: 50 | ; Country: USSR/CIS 51 | ; Manufacturer: AvtoVAZ 52 | ; Region: Europe 53 | ; Years: 2018, 1988 54 | 55 | ; Details: 56 | ; Body: Station Wagon, 5-Door 57 | ; Engine: 21179 58 | ; Model: Vesta 59 | ; Plant: Izhevsk 60 | ; Serial: 144213 61 | ; Transmission: Manual Renault 62 | 63 | ; Verify checksum 64 | $ vininfo check 1M8GDM9AXKP042788 65 | ; Checksum is valid 66 | ``` 67 | 68 | ### Python 69 | 70 | ```bash 71 | from vininfo import Vin 72 | 73 | vin = Vin('VF1LM1B0H36666155') 74 | 75 | vin.country # France 76 | vin.manufacturer # Renault 77 | vin.region # Europe 78 | vin.wmi # VF1 79 | vin.vds # LM1B0H 80 | vin.vis # 36666155 81 | 82 | annotated = vin.annotate() 83 | details = vin.details 84 | 85 | vin.verify_checksum() # False 86 | Vin('1M8GDM9AXKP042788').verify_checksum() # True 87 | ``` 88 | 89 | ## Development 90 | 91 | One can add missing WMI(s) using instructions from `dicts/wmi.py`: 92 | `WMI` dictionary, that maps WMI strings to manufacturers. 93 | 94 | Those manufacturers may be represented by simple strings, or instances of `Brand` 95 | subclasses (see `brands.py`). 96 | 97 | If you know how to decode additional information (model, body, engine, etc.) 98 | encoded in VIN, you may also want to create a so-called `details extractor` 99 | for a brand. 100 | 101 | Details extractors are `VinDetails` subclasses in most cases making use of 102 | `Detail` descriptors to represent additional information 103 | (see `details/nissan.py` for example). 104 | -------------------------------------------------------------------------------- /src/vininfo/details/opel.py: -------------------------------------------------------------------------------- 1 | from ..dicts.bodies import * 2 | from ._base import Detail, VinDetails 3 | 4 | 5 | def get_engine(details): 6 | 7 | candidates = { 8 | 'P': { 9 | '1': 'A18XER140HP', 10 | 'B': 'A14XER100HP', 11 | 'C': 'A14NET140HP', 12 | 'D': 'A16XER115HP', 13 | 'J': 'A16LET180HP', 14 | 'N': 'A20DTH165HP', 15 | 'U': 'A14NEL120HP', 16 | }, 17 | 'W': { 18 | 'N': 'A20DTH160HP', 19 | 'C': 'A14NET140HP', 20 | }, 21 | 'G': { 22 | 'A': 'A16XER115HP', 23 | 'B': 'A16LET180HP', 24 | 'C': 'A18XER140HP', 25 | 'D': 'A20NHT220HP', 26 | 'E': 'A20NHT250HP', 27 | 'F': 'A28NET260HP', 28 | 'G': 'A28NER325HP', 29 | 'M': 'A20DTH160HP', 30 | 'N': 'A20DTR195HP', 31 | 'P': 'A14NFT140HP', 32 | 'X': 'A20NHT250HP', 33 | }, 34 | 'V': { 35 | '1': 'Y13D70HP', 36 | '2': 'Z14XEP100HP', 37 | }, 38 | 'S': { 39 | 'C': 'Z14XEP100HP', 40 | 'D': 'A14NEL120HP', 41 | 'E': 'A14NET140HP', 42 | }, 43 | 'J': { 44 | '8': 'A14NET140HP', 45 | }, 46 | } 47 | 48 | return candidates.get(details.model.code, {}) 49 | 50 | 51 | class OpelDetails(VinDetails): 52 | """Opel VIN details extractor.""" 53 | 54 | model = Detail(('vds', 0), { 55 | 'F': 'Agila', 56 | 'G': 'Insignia', 57 | 'J': 'Mokka', 58 | 'L': 'Antara', 59 | 'M': 'Movano', 60 | 'P': ['Astra J', 'Zafira C'], 61 | 'R': 'Astra GTC J', 62 | 'S': 'Meriva', 63 | 'V': 'Combo II', 64 | 'W': 'Cascada', 65 | }) 66 | 67 | body = Detail(('vds', 2), { 68 | '2': BODY_HATCH_3, 69 | '3': BODY_COUPE_2, 70 | '5': BODY_SEDAN_4, 71 | '6': BODY_HATCH_5, 72 | '7': BODY_CROSS_3, 73 | '8': BODY_SW_5, 74 | '9': BODY_MINIVAN_5, 75 | 'B': BODY_MINIBUS, 76 | 'C': BODY_VAN, 77 | 'J': BODY_VAN, 78 | 'X': BODY_SW_3, 79 | }) 80 | 81 | engine = Detail(('vds', 4), get_engine) 82 | 83 | plant = Detail(('vis', 1), { 84 | '1': 'Russelsheim', 85 | '2': 'Bochum', 86 | '3': 'Azambuja', 87 | '4': 'Zaragoza', 88 | '5': 'Antwerp', 89 | '6': 'Eisenach', 90 | '7': 'Luton', 91 | '8': 'Ellesmere Port', 92 | '9': 'Uusikaupunki', 93 | 'A': 'Azambuja', 94 | 'B': 'Bertone / Saint Peterburg', 95 | 'C': 'Yelabuga', 96 | 'D': 'Shin Chuang', 97 | 'E': 'Heuliez', 98 | 'F': 'Togliatti', 99 | 'G': 'Gliwice', 100 | 'H': 'Rayong', 101 | 'L': 'Elizabeth', 102 | 'M': 'Millbrook', 103 | 'N': 'Norwich', 104 | 'P': 'Warsaw', 105 | 'R': 'Rosario', 106 | 'S': 'Szentgotthard', 107 | 'V': 'Luton', 108 | 'X': 'Zaporozhia', 109 | 'Z': 'Izmir', 110 | }) 111 | 112 | serial = Detail(('vis', slice(2, None))) 113 | -------------------------------------------------------------------------------- /src/vininfo/dicts/countries.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def __unpack_countries_map(counties: dict[str, str]) -> dict[str, str]: 4 | unpacked = {} 5 | 6 | seq = 'ABCDEFGHJKLMNPRSTUVWXYZ1234567890' 7 | 8 | for code, title in counties.items(): 9 | first, _, span = code.partition('-') 10 | 11 | if span: 12 | ch_from, ch_till = span 13 | 14 | else: 15 | ch_from = 'A' 16 | ch_till = '0' 17 | 18 | for ch in seq[seq.index(ch_from):seq.index(ch_till) + 1]: 19 | unpacked[first + ch] = title 20 | 21 | return unpacked 22 | 23 | 24 | # ISO 3780 https://www.iso.org/standard/45844.html 25 | COUNTRIES = __unpack_countries_map({ 26 | 'A-AH': 'South Africa', 27 | 'A-JK': 'Ivory Coast', 28 | 'A-LM': 'Lesotho', 29 | 'A-NP': 'Botswana', 30 | 'A-RS': 'Namibia', 31 | 'A-TU': 'Madagascar', 32 | 'A-VW': 'Mauritius', 33 | 'A-XY': 'Tunisia', 34 | 'A-Z1': 'Cyprus', 35 | 'A-23': 'Zimbabwe', 36 | 'A-45': 'Mozambique', 37 | 'B-AB': 'Angola', 38 | 'B-FG': 'Kenya', 39 | 'B-LL': 'Nigeria', 40 | 'B-RR': 'Algeria', 41 | 'B-34': 'Libya', 42 | 'C-AB': 'Egypt', 43 | 'C-FG': 'Morocco', 44 | 'C-LM': 'Zambia', 45 | 'D-AE': 'Egypt', 46 | 'H-': 'China', 47 | 'J-': 'Japan', 48 | 'K-FH': 'Israel', 49 | 'K-LR': 'South Korea', 50 | 'K-ST': 'Jordan', 51 | 'L-': 'China', 52 | 'M-AE': 'India', 53 | 'M-FK': 'Indonesia', 54 | 'M-LR': 'Thailand', 55 | 'M-SS': 'Myanmar', 56 | 'M-UU': 'Mongolia', 57 | 'M-XX': 'Kazakhstan', 58 | 'M-16': 'India', 59 | 'N-AE': 'Iran', 60 | 'N-FG': 'Pakistan', 61 | 'N-JJ': 'Iraq', 62 | 'N-LR': 'Turkey', 63 | 'N-ST': 'Uzbekistan', 64 | 'N-UU': 'Azerbaijan', 65 | 'N-YY': 'Armenia', 66 | 'N-15': 'Iran', 67 | 'P-AC': 'Philippines', 68 | 'P-FG': 'Singapore', 69 | 'P-LR': 'Malaysia', 70 | 'P-ST': 'Bangladesh', 71 | 'R-AB': 'United Arab Emirates', 72 | 'R-FK': 'Taiwan', 73 | 'R-LN': 'Vietnam', 74 | 'R-PP': 'Laos', 75 | 'R-ST': 'Saudi Arabia', 76 | 'R-UW': 'Russia', 77 | 'R-11': 'Hong Kong', 78 | 'S-AM': 'United Kingdom', 79 | 'S-NT': 'Germany', 80 | 'S-UZ': 'Poland', 81 | 'S-12': 'Latvia', 82 | 'S-33': 'South Ossetia', 83 | 'T-AH': 'Switzerland', 84 | 'T-JP': 'Czech Republic', 85 | 'T-RV': 'Hungary', 86 | 'T-W2': 'Portugal', 87 | 'T-35': 'Serbia & Montenegro', 88 | 'T-66': 'Andorra', 89 | 'U-AC': 'Spain', 90 | 'U-HM': 'Denmark', 91 | 'U-NR': 'Ireland', 92 | 'U-UW': 'Romania', 93 | 'U-12': 'Macedonia', 94 | 'U-57': 'Slovakia', 95 | 'U-80': 'Bosnia & Herzogovina', 96 | 'V-AE': 'Austria', 97 | 'V-FR': 'France', 98 | 'V-SW': 'Spain', 99 | 'V-X2': 'Serbia', 100 | 'V-35': 'Croatia', 101 | 'V-68': 'Estonia', 102 | 'W-': 'Germany', 103 | 'X-AC': 'Bulgaria', 104 | 'X-DE': 'Russia', 105 | 'X-FH': 'Greece', 106 | 'X-JK': 'Russia', 107 | 'X-LR': 'Netherlands', 108 | 'X-SW': 'Russia', 109 | 'X-XY': 'Luxembourg', 110 | 'X-Z0': 'Russia', 111 | 'Y-AE': 'Belgium', 112 | 'Y-FK': 'Finland', 113 | 'Y-NN': 'Malta', 114 | 'Y-SW': 'Sweden', 115 | 'Y-X2': 'Norway', 116 | 'Y-35': 'Belarus', 117 | 'Y-68': 'Ukraine', 118 | 'Z-AU': 'Italy', 119 | 'Z-XZ': 'Slovenia', 120 | 'Z-11': 'San Marino', 121 | 'Z-35': 'Lithuania', 122 | 'Z-60': 'Russia', 123 | '1-': 'United States', 124 | '2-A5': 'Canada', 125 | '3-AX': 'Mexico', 126 | '3-55': 'Dom. Republic', 127 | '3-66': 'Honduras', 128 | '3-77': 'Panama', 129 | '3-89': 'Puerto Rico', 130 | '4-': 'United States', 131 | '5-': 'United States', 132 | '6-AX': 'Australia', 133 | '6-Y1': 'New Zealand', 134 | '7-': 'United States', 135 | '8-AE': 'Argentina', 136 | '8-FG': 'Chile', 137 | '8-LN': 'Ecuador', 138 | '8-ST': 'Peru', 139 | '8-XZ': 'Venezuela', 140 | '8-22': 'Bolivia', 141 | '9-AE': 'Brazil', 142 | '9-FG': 'Colombia', 143 | '9-SV': 'Uruguay', 144 | '9-29': 'Brazil', 145 | }) 146 | -------------------------------------------------------------------------------- /tests/test_dafra.py: -------------------------------------------------------------------------------- 1 | from vininfo import Vin 2 | 3 | base_expected = { 4 | 'wmi': '95V', 5 | 'region_code': '9', 6 | 'region': 'South America', 7 | 'country_code': '95', 8 | 'country': 'Brazil', 9 | 'assembler': 'Dafra (Dafra)', 10 | 'manufacturer': 'Dafra', 11 | 'brand': 'Dafra (Dafra)', 12 | 'body_code': '', 13 | 'body_name': 'Motorcycle', 14 | 'plant_code': 'M', 15 | 'plant_name': 'Manaus', 16 | } 17 | 18 | def data_provider(): 19 | return [ 20 | { 21 | 'vin': '95VCB1K589M017683', 22 | 'expected': { 23 | 'vds': 'CB1K58', 24 | 'vis': '9M017683', 25 | 'serial_code': '017683', 26 | 'squish_vin': '95VCB1K59M', 27 | 'years_code': '9', 28 | 'years': [2009], 29 | 'model_code': 'CB', 30 | 'model_name': 'Kansas 150', 31 | **base_expected 32 | } 33 | }, 34 | { 35 | 'vin': '95VCB1K589M022466', 36 | 'expected': { 37 | 'vds': 'CB1K58', 38 | 'vis': '9M022466', 39 | 'serial_code': '022466', 40 | 'squish_vin': '95VCB1K59M', 41 | 'years_code': '9', 42 | 'years': [2009], 43 | 'model_code': 'CB', 44 | 'model_name': 'Kansas 150', 45 | **base_expected 46 | } 47 | }, 48 | { 49 | 'vin': '95VCA1C899M010049', 50 | 'expected': { 51 | 'vds': 'CA1C89', 52 | 'vis': '9M010049', 53 | 'serial_code': '010049', 54 | 'squish_vin': '95VCA1C89M', 55 | 'years_code': '9', 56 | 'years': [2009], 57 | 'model_code': 'CA', 58 | 'model_name': 'Speed 150', 59 | **base_expected 60 | } 61 | }, 62 | { 63 | 'vin': '95VCA4A8BBM001656', 64 | 'expected': { 65 | 'vds': 'CA4A8B', 66 | 'vis': 'BM001656', 67 | 'serial_code': '001656', 68 | 'squish_vin': '95VCA4A8BM', 69 | 'years_code': 'B', 70 | 'years': [2011, 1981], 71 | 'model_code': 'CA', 72 | 'model_name': 'Speed 150', 73 | **base_expected 74 | } 75 | }, 76 | ] 77 | 78 | 79 | def test_dafra(): 80 | for data in data_provider(): 81 | vin = data.get('vin') 82 | expected = data.get('expected') 83 | vin = Vin(vin) 84 | 85 | assert f'{vin}' 86 | assert vin.wmi == expected.get('wmi'), f'For {vin}' 87 | assert vin.manufacturer == expected.get('manufacturer'), f'For {vin}' 88 | assert f'{vin.assembler}' == expected.get('assembler'), f'For {vin}' 89 | assert vin.vds == expected.get('vds'), f'For {vin}' 90 | assert vin.vis == expected.get('vis'), f'For {vin}' 91 | assert vin.years_code == expected.get('years_code'), f'For {vin}' 92 | assert vin.years == expected.get('years'), f'For {vin}' 93 | assert vin.region_code == expected.get('region_code'), f'For {vin}' 94 | assert vin.region == expected.get('region'), f'For {vin}' 95 | assert vin.country_code == expected.get('country_code'), f'For {vin}' 96 | assert vin.country == expected.get('country'), f'For {vin}' 97 | assert f'{vin.brand}' == expected.get('brand'), f'For {vin}' 98 | assert vin.squish_vin == expected.get('squish_vin'), f'For {vin}' 99 | 100 | details = vin.details 101 | assert details.model.code == expected.get('model_code'), f'For {vin}' 102 | assert details.model.name == expected.get('model_name'), f'For {vin}' 103 | assert details.body.code == expected.get('body_code'), f'For {vin}' 104 | assert details.body.name == expected.get('body_name'), f'For {vin}' 105 | assert not details.engine, f'For {vin}' 106 | assert not details.transmission, f'For {vin}' 107 | assert details.plant.code == expected.get('plant_code'), f'For {vin}' 108 | assert details.plant.name == expected.get('plant_name'), f'For {vin}' 109 | assert details.serial.code == expected.get('serial_code'), f'For {vin}' 110 | -------------------------------------------------------------------------------- /src/vininfo/toolbox.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, timezone 3 | from typing import TYPE_CHECKING, ClassVar 4 | 5 | from .common import Annotatable, Assembler, Brand, UnsupportedBrand 6 | from .dicts import COUNTRIES, REGIONS, WMI 7 | from .exceptions import ValidationError 8 | 9 | if TYPE_CHECKING: 10 | from .details._base import VinDetails 11 | 12 | 13 | class Vin(Annotatable): 14 | """Offers basic VIN data extraction facilities.""" 15 | 16 | annotate_titles: ClassVar = { 17 | 'manufacturer': 'Manufacturer', 18 | 'region': 'Region', 19 | 'country': 'Country', 20 | 'years': 'Years', 21 | } 22 | 23 | def __init__(self, num: str): 24 | self._brand = None 25 | self.num = self.validate(num) 26 | 27 | _details = None 28 | for brand in self.assembler.brands: 29 | if isinstance(brand, str): 30 | brand = Brand(brand) 31 | details_extractor = brand.extractor 32 | 33 | if details_extractor: 34 | _details = details_extractor(self) 35 | if _details.model and _details.model.name: 36 | self._brand = brand 37 | break 38 | 39 | self.details: VinDetails = _details 40 | 41 | def __str__(self): 42 | return self.num 43 | 44 | @classmethod 45 | def validate(cls, num: str) -> str: 46 | """Performs basic VIN validation and sanation. 47 | 48 | :param num: 49 | 50 | """ 51 | num = num.strip().upper() 52 | 53 | num_len = len(num) 54 | if num_len != 17: 55 | raise ValidationError(f'VIN number requires 17 chars ({num_len} given)') 56 | 57 | pattern = r"^[A-HJ-NPR-Z0-9]{17}$" 58 | if not re.match(pattern, num): 59 | raise ValidationError("VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' ") 60 | 61 | return num 62 | 63 | def verify_checksum(self, *, check_year: bool = True) -> bool: 64 | """Performs checksum verification. 65 | 66 | .. warning:: Not every manufacturer uses VIN checksum rules. 67 | 68 | :param check_year: Whether to also check the model year. 69 | Note that not all manufacturer abey the rule. Default: True. 70 | 71 | """ 72 | if check_year and self.vis[0] in {'U', 'Z', '0'}: 73 | return False 74 | 75 | trans = { 76 | 'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7, 'H': 8, 77 | 'J': 1, 'K': 2, 'L': 3, 'M': 4, 'N': 5, 'P': 7, 'R': 9, 78 | 'S': 2, 'T': 3, 'U': 4, 'V': 5, 'W': 6, 'X': 7, 'Y': 8, 'Z': 9, 79 | } 80 | weights = (8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2) 81 | 82 | checksum = 0 83 | 84 | for pos, char in enumerate(self.num): 85 | value = int(trans.get(char, char)) 86 | checksum += (value * weights[pos]) 87 | 88 | checksum = int(checksum) % 11 89 | 90 | check_digit = 'X' if checksum == 10 else checksum 91 | 92 | return f'{check_digit}' == self.vds[5] 93 | 94 | @property 95 | def wmi(self) -> str: 96 | """WMI (World Manufacturers Identification)""" 97 | return self.num[:3] 98 | 99 | @property 100 | def assembler(self) -> Assembler: 101 | """Assembler object.""" 102 | 103 | wmi = self.wmi 104 | 105 | assembler = WMI.get(wmi) 106 | 107 | if not assembler: 108 | assembler = WMI.get(wmi[:2]) 109 | 110 | if isinstance(assembler, str): 111 | assembler = Brand(assembler) 112 | 113 | if assembler is None: 114 | assembler = UnsupportedBrand() 115 | 116 | return assembler 117 | 118 | @property 119 | def brand(self) -> Brand: 120 | """Brand object.""" 121 | brand = self._brand 122 | return UnsupportedBrand() if brand is None else brand 123 | 124 | @property 125 | def manufacturer(self) -> str: 126 | """Manufacturer title.""" 127 | return self.assembler.manufacturer 128 | 129 | @property 130 | def manufacturer_is_small(self) -> bool: 131 | """A manufacturer who builds fewer than 1000 vehicles per year.""" 132 | return f'{self.wmi[2]}' == '9' 133 | 134 | @property 135 | def vds(self) -> str: 136 | """VDS (Vehicle Descriptor Section)""" 137 | return self.num[3:9] 138 | 139 | @property 140 | def vis(self) -> str: 141 | """VIS (Vehicle Identifier Section)""" 142 | return self.num[9:17] 143 | 144 | @property 145 | def region_code(self) -> str: 146 | return self.wmi[0] 147 | 148 | @property 149 | def region(self) -> str | None: 150 | code = self.region_code 151 | 152 | title = None 153 | 154 | for chars, title_ in REGIONS.items(): 155 | if code in chars: 156 | title = title_ 157 | break 158 | 159 | return title 160 | 161 | @property 162 | def country_code(self) -> str: 163 | return self.wmi[0:2] 164 | 165 | @property 166 | def country(self) -> str | None: 167 | return COUNTRIES.get(self.country_code) 168 | 169 | @property 170 | def years_code(self) -> str: 171 | return self.vis[0] 172 | 173 | @property 174 | def years(self) -> list[int]: 175 | letters = 'ABCDEFGHJKLMNPRSTVWXY123456789' 176 | overflow_delta = len(letters) 177 | start_year_iso_table = 1980 178 | net_year = datetime.now(tz=timezone.utc).year + 1 179 | 180 | try: 181 | # E.g. .years_code == O (oh) 182 | delta = letters.index(self.years_code) 183 | except ValueError: 184 | return [] 185 | 186 | year = delta + start_year_iso_table 187 | result = [year] 188 | while year + overflow_delta <= net_year: 189 | year += overflow_delta 190 | result.append(year) 191 | 192 | result.sort(reverse=True) 193 | 194 | return result 195 | 196 | @property 197 | def squish_vin(self) -> str: 198 | """Squish (or Pattern) VIN. 199 | 200 | The first 11 digits of the VIN minus the 9th digit (positions 1-8, 201 | positions 10 and 11). 202 | 203 | Squish VIN encodes vehicle information while preventing its precise 204 | identification. It can be useful for anonymization and privacy purposes. 205 | """ 206 | return self.num[:8] + self.num[9:11] 207 | -------------------------------------------------------------------------------- /tests/test_bajaj.py: -------------------------------------------------------------------------------- 1 | from vininfo import Vin 2 | 3 | base_expected = { 4 | 'brand': 'Bajaj (Bajaj)', 5 | 'body_code': '', 6 | 'body_name': 'Motorcycle', 7 | } 8 | 9 | brazil_base_expected = { 10 | 'wmi': '92T', 11 | 'manufacturer': 'Bajaj', 12 | 'region_code': '9', 13 | 'region': 'South America', 14 | 'country_code': '92', 15 | 'country': 'Brazil', 16 | 'assembler': 'Bajaj (Bajaj)', 17 | 'plant_code': 'M', 18 | 'plant_name': 'Manaus', 19 | **base_expected 20 | } 21 | 22 | dafra_base_expected = { 23 | 'wmi': '95V', 24 | 'manufacturer': 'Dafra', 25 | 'region_code': '9', 26 | 'region': 'South America', 27 | 'country_code': '95', 28 | 'country': 'Brazil', 29 | 'assembler': 'Dafra (Dafra)', 30 | 'plant_code': 'M', 31 | 'plant_name': 'Manaus', 32 | **base_expected 33 | } 34 | 35 | india_base_expected = { 36 | 'wmi': 'MD2', 37 | 'manufacturer': 'Bajaj', 38 | 'region_code': 'M', 39 | 'region': 'Asia', 40 | 'country_code': 'MD', 41 | 'country': 'India', 42 | 'assembler': 'Bajaj (Bajaj)', 43 | 'plant_code': 'C', 44 | 'plant_name': 'Chakan', 45 | **base_expected 46 | } 47 | 48 | 49 | def data_provider(): 50 | return [ 51 | { 52 | 'vin': '95V2A1E5PPM099209', 53 | 'expected': { 54 | 'vds': '2A1E5P', 55 | 'vis': 'PM099209', 56 | 'serial_code': '099209', 57 | 'squish_vin': '95V2A1E5PM', 58 | 'years_code': 'P', 59 | 'years': [2023, 1993], 60 | 'model_code': '2A1', 61 | 'model_name': 'Dominar 160', 62 | **dafra_base_expected 63 | } 64 | }, 65 | { 66 | 'vin': '95V2B1K5NPM099112', 67 | 'expected': { 68 | 'vds': '2B1K5N', 69 | 'vis': 'PM099112', 70 | 'serial_code': '099112', 71 | 'squish_vin': '95V2B1K5PM', 72 | 'years_code': 'P', 73 | 'years': [2023, 1993], 74 | 'model_code': '2B1', 75 | 'model_name': 'Dominar 200', 76 | **dafra_base_expected 77 | } 78 | }, 79 | { 80 | 'vin': '95V3B1J5NPM099022', 81 | 'expected': { 82 | 'vds': '3B1J5N', 83 | 'vis': 'PM099022', 84 | 'serial_code': '099022', 85 | 'squish_vin': '95V3B1J5PM', 86 | 'years_code': 'P', 87 | 'years': [2023, 1993], 88 | 'model_code': '3B1', 89 | 'model_name': 'Dominar 400', 90 | **dafra_base_expected 91 | } 92 | }, 93 | { 94 | 'vin': '92TA92DZXRMC09006', 95 | 'expected': { 96 | 'vds': 'A92DZX', 97 | 'vis': 'RMC09006', 98 | 'serial_code': 'C09006', 99 | 'squish_vin': '92TA92DZRM', 100 | 'years_code': 'R', 101 | 'years': [2024, 1994], 102 | 'model_code': 'A92', 103 | 'model_name': 'Dominar 160', 104 | **brazil_base_expected 105 | } 106 | }, 107 | { 108 | 'vin': '92TA92DX4TML92419', 109 | 'expected': { 110 | 'vds': 'A92DX4', 111 | 'vis': 'TML92419', 112 | 'serial_code': 'L92419', 113 | 'squish_vin': '92TA92DXTM', 114 | 'years_code': 'T', 115 | 'years': [2026, 1996], 116 | 'model_code': 'A92', 117 | 'model_name': 'Dominar NS160', 118 | **brazil_base_expected 119 | } 120 | }, 121 | { 122 | 'vin': '92TA36FZ8RMC90909', 123 | 'expected': { 124 | 'vds': 'A36FZ8', 125 | 'vis': 'RMC90909', 126 | 'serial_code': 'C90909', 127 | 'squish_vin': '92TA36FZRM', 128 | 'years_code': 'R', 129 | 'years': [2024, 1994], 130 | 'model_code': 'A36', 131 | 'model_name': 'Dominar 200', 132 | **brazil_base_expected 133 | } 134 | }, 135 | { 136 | 'vin': '92TA36FX9TML92914', 137 | 'expected': { 138 | 'vds': 'A36FX9', 139 | 'vis': 'TML92914', 140 | 'serial_code': 'L92914', 141 | 'squish_vin': '92TA36FXTM', 142 | 'years_code': 'T', 143 | 'years': [2026, 1996], 144 | 'model_code': 'A36', 145 | 'model_name': 'Dominar NS200', 146 | **brazil_base_expected 147 | } 148 | }, 149 | { 150 | 'vin': '92TB65GZ1RMD90922', 151 | 'expected': { 152 | 'vds': 'B65GZ1', 153 | 'vis': 'RMD90922', 154 | 'serial_code': 'D90922', 155 | 'squish_vin': '92TB65GZRM', 156 | 'years_code': 'R', 157 | 'years': [2024, 1994], 158 | 'model_code': 'B65', 159 | 'model_name': 'Dominar 250', 160 | **brazil_base_expected 161 | } 162 | }, 163 | { 164 | 'vin': '92TA67MZ1RMC90909', 165 | 'expected': { 166 | 'vds': 'A67MZ1', 167 | 'vis': 'RMC90909', 168 | 'serial_code': 'C90909', 169 | 'squish_vin': '92TA67MZRM', 170 | 'years_code': 'R', 171 | 'years': [2024, 1994], 172 | 'model_code': 'A67', 173 | 'model_name': 'Dominar 400', 174 | **brazil_base_expected 175 | } 176 | }, 177 | { 178 | 'vin': '92TC41CX1TMB90948', 179 | 'expected': { 180 | 'vds': 'C41CX1', 181 | 'vis': 'TMB90948', 182 | 'serial_code': 'B90948', 183 | 'squish_vin': '92TC41CXTM', 184 | 'years_code': 'T', 185 | 'years': [2026, 1996], 186 | 'model_code': 'C41', 187 | 'model_name': 'Pulsar N150', 188 | **brazil_base_expected 189 | } 190 | }, 191 | { 192 | 'vin': 'MD2A67MXXRCK99693', 193 | 'expected': { 194 | 'vds': 'A67MXX', 195 | 'vis': 'RCK99693', 196 | 'serial_code': 'K99693', 197 | 'squish_vin': 'MD2A67MXRC', 198 | 'years_code': 'R', 199 | 'years': [2024, 1994], 200 | 'model_code': 'A67', 201 | 'model_name': 'Dominar 400', 202 | **india_base_expected 203 | } 204 | }, 205 | { 206 | 'vin': 'MD2B65GX9PCF91987', 207 | 'expected': { 208 | 'vds': 'B65GX9', 209 | 'vis': 'PCF91987', 210 | 'serial_code': 'F91987', 211 | 'squish_vin': 'MD2B65GXPC', 212 | 'years_code': 'P', 213 | 'years': [2023, 1993], 214 | 'model_code': 'B65', 215 | 'model_name': 'Dominar 250', 216 | **india_base_expected 217 | } 218 | }, 219 | { 220 | 'vin': 'MD2B97FX1PCB96793', 221 | 'expected': { 222 | 'vds': 'B97FX1', 223 | 'vis': 'PCB96793', 224 | 'serial_code': 'B96793', 225 | 'squish_vin': 'MD2B97FXPC', 226 | 'years_code': 'P', 227 | 'years': [2023, 1993], 228 | 'model_code': 'B97', 229 | 'model_name': 'Pulsar N250', 230 | **india_base_expected 231 | } 232 | }, 233 | { 234 | 'vin': 'MD2B54DX5PCB94941', 235 | 'expected': { 236 | 'vds': 'B54DX5', 237 | 'vis': 'PCB94941', 238 | 'serial_code': 'B94941', 239 | 'squish_vin': 'MD2B54DXPC', 240 | 'years_code': 'P', 241 | 'years': [2023, 1993], 242 | 'model_code': 'B54', 243 | 'model_name': 'Pulsar N160', 244 | **india_base_expected 245 | } 246 | }, 247 | ] 248 | 249 | 250 | def test_bajaj(): 251 | for data in data_provider(): 252 | vin = data.get('vin') 253 | expected = data.get('expected') 254 | vin = Vin(vin) 255 | 256 | assert f'{vin}' 257 | assert vin.wmi == expected.get('wmi'), f'For {vin}' 258 | assert vin.manufacturer == expected.get('manufacturer'), f'For {vin}' 259 | assert f'{vin.assembler}' == expected.get('assembler'), f'For {vin}' 260 | assert vin.vds == expected.get('vds'), f'For {vin}' 261 | assert vin.vis == expected.get('vis'), f'For {vin}' 262 | assert vin.years_code == expected.get('years_code'), f'For {vin}' 263 | assert vin.years == expected.get('years'), f'For {vin}' 264 | assert vin.region_code == expected.get('region_code'), f'For {vin}' 265 | assert vin.region == expected.get('region'), f'For {vin}' 266 | assert vin.country_code == expected.get('country_code'), f'For {vin}' 267 | assert vin.country == expected.get('country'), f'For {vin}' 268 | assert f'{vin.brand}' == expected.get('brand'), f'For {vin}' 269 | assert vin.squish_vin == expected.get('squish_vin'), f'For {vin}' 270 | 271 | details = vin.details 272 | assert details.model.code == expected.get('model_code'), f'For {vin}' 273 | assert details.model.name == expected.get('model_name'), f'For {vin}' 274 | assert details.body.code == expected.get('body_code'), f'For {vin}' 275 | assert details.body.name == expected.get('body_name'), f'For {vin}' 276 | assert not details.engine, f'For {vin}' 277 | assert not details.transmission, f'For {vin}' 278 | assert details.plant.code == expected.get('plant_code'), f'For {vin}' 279 | assert details.plant.name == expected.get('plant_name'), f'For {vin}' 280 | assert details.serial.code == expected.get('serial_code'), f'For {vin}' 281 | -------------------------------------------------------------------------------- /src/vininfo/dicts/wmi.py: -------------------------------------------------------------------------------- 1 | from ..assemblers import Dafra 2 | from ..brands import Bajaj, Lada, Nissan, Opel, Renault 3 | 4 | # NOTE: 5 | # if you want to extend this mapping with new WMIs, please use 6 | # vininfo.utils.merge_wmi() function and replace the entire WMI with 7 | # the result of this function. This comment must stay intact. 8 | WMI = { 9 | '000': 'Maserati', 10 | '04W': 'Buick', 11 | '0VF': 'Ford', 12 | '112': 'Volkswagen', 13 | '115': 'Mercedes-Benz', 14 | '117': 'Volkswagen', 15 | '119': 'Replica/Kit Makes', 16 | '123': 'Mercedes-Benz', 17 | '124': 'Chevrolet', 18 | '137': 'AM', 19 | '178': 'Jaguar', 20 | '17V': 'Ford', 21 | '19': 'Acura', 22 | '19X': 'Honda', 23 | '1A4': 'Chrysler', 24 | '1A8': 'Chrysler', 25 | '1B': 'Dodge', 26 | '1C': 'Chrysler', 27 | '1C4': 'Dodge', 28 | '1C6': 'Ram', 29 | '1C8': 'Chrysler', 30 | '1CN': 'Lincoln', 31 | '1D': 'Dodge', 32 | '1F': 'Ford', 33 | '1F2': 'Subaru', 34 | '1F9': 'FWD Corp.', 35 | '1FU': 'Freightliner', 36 | '1FV': 'Freightliner', 37 | '1G': 'General Motors', 38 | '1G1': 'Chevrolet', 39 | '1G2': 'Pontiac', 40 | '1G3': 'Oldsmobile', 41 | '1G4': 'Buick', 42 | '1G6': 'Cadillac', 43 | '1G8': 'Saturn', 44 | '1G9': 'Google', 45 | '1GA': 'Chevrolet', 46 | '1GB': 'Chevrolet USA', 47 | '1GC': 'Chevrolet', 48 | '1GD': 'GMC', 49 | '1GE': 'Cadillac', 50 | '1GG': 'Isuzu', 51 | '1GH': 'Oldsmobile', 52 | '1GJ': 'GMC', 53 | '1GK': 'GMC', 54 | '1GM': 'Pontiac', 55 | '1GN': 'Chevrolet USA', 56 | '1GT': 'GMC Truck', 57 | '1GY': 'Cadillac', 58 | '1H': 'Honda', 59 | '1HD': 'Harley-Davidson', 60 | '1J': 'Jeep', 61 | '1J4': 'Jeep', 62 | '1J8': 'Jeep', 63 | '1JN': Nissan(), 64 | '1L': 'Lincoln', 65 | '1L1': 'Lincoln', 66 | '1L4': 'Jeep', 67 | '1LN': 'Lincoln', 68 | '1M': 'Mercury', 69 | '1M1': 'Mack Truck', 70 | '1M2': 'Mack Truck', 71 | '1M3': 'Mack Truck', 72 | '1M4': 'Mack Truck', 73 | '1M9': 'Mynatt Truck & Equipment', 74 | '1ME': 'Mercury', 75 | '1N': Nissan(), 76 | '1NX': 'NUMMI', 77 | '1P3': 'Plymouth', 78 | '1P4': 'Plymouth', 79 | '1R9': 'Roadrunner Hay Squeeze', 80 | '1TK': 'Scion', 81 | '1V1': 'Volkswagen USA (Commercials)', 82 | '1V2': 'Volkswagen', 83 | '1V4': Nissan(), 84 | '1VW': 'Volkswagen', 85 | '1XK': 'Kenworth', 86 | '1XP': 'Peterbilt', 87 | '1Y1': 'Chevrolet', 88 | '1YV': 'Mazda', 89 | '1Z3': 'Mitsubishi', 90 | '1Z7': 'Mitsubishi', 91 | '1ZV': 'Auto Alliance International', 92 | '1ZW': 'Mercury', 93 | '210': 'Ford', 94 | '2A4': 'Chrysler Canada', 95 | '2A8': 'Chrysler Canada', 96 | '2B3': 'Dodge Canada', 97 | '2B4': 'Dodge', 98 | '2B5': 'Dodge', 99 | '2B6': 'Dodge', 100 | '2B7': 'Dodge', 101 | '2B8': 'Dodge', 102 | '2C3': 'Chrysler', 103 | '2C4': 'Chrysler Canada', 104 | '2C7': 'Dodge', 105 | '2C8': 'Chrysler', 106 | '2CK': 'Pontiac', 107 | '2CN': 'CAMI', 108 | '2CT': 'General Motors', 109 | '2D3': 'Dodge', 110 | '2D4': 'Dodge Canada', 111 | '2D6': 'Dodge', 112 | '2D7': 'Dodge', 113 | '2D8': 'Dodge Canada', 114 | '2DG': 'Ontario Drive & Gear', 115 | '2F': 'Ford', 116 | '2FU': 'Freightliner', 117 | '2FV': 'Freightliner', 118 | '2FZ': 'Sterling', 119 | '2G': 'General Motors', 120 | '2G1': 'Chevrolet', 121 | '2G2': 'Pontiac', 122 | '2G3': 'Oldsmobile', 123 | '2G4': 'Buick', 124 | '2G6': 'Cadillac', 125 | '2G9': 'Gnome Homes', 126 | '2GC': 'Chevrolet Canada', 127 | '2GE': 'Cadillac', 128 | '2GK': 'GMC', 129 | '2GN': 'Chevrolet Canada', 130 | '2GT': 'GMC', 131 | '2H': 'Honda', 132 | '2HH': 'Acura', 133 | '2HM': 'Hyundai', 134 | '2HN': 'Acura', 135 | '2L': 'Lincoln', 136 | '2L1': 'Lincoln', 137 | '2LM': 'Lincoln', 138 | '2LN': 'Lincoln', 139 | '2M': 'Mercury', 140 | '2NV': 'Nova Bus', 141 | '2P3': 'Plymouth', 142 | '2P4': 'Plymouth', 143 | '2S2': 'Suzuki', 144 | '2S3': 'Suzuki Canada', 145 | '2T': 'Toyota', 146 | '2T2': 'Lexus Canada', 147 | '2V4': 'Volkswagen', 148 | '2V8': 'Volkswagen', 149 | '2W': 'Western Star', 150 | '309': 'Chevrolet', 151 | '3A': 'Chrysler Mexico', 152 | '3A4': 'Chrysler', 153 | '3A8': 'Chrysler', 154 | '3B6': 'Dodge', 155 | '3B7': 'Dodge Mexico', 156 | '3C': 'Chrysler', 157 | '3C3': 'Fiat', 158 | '3C4': 'Dodge Mexico', 159 | '3C6': 'Ram', 160 | '3C7': 'Ram', 161 | '3C8': 'Chrysler', 162 | '3CZ': 'Honda Mexico', 163 | '3D': 'Dodge', 164 | '3F': 'Ford', 165 | '3G': 'General Motors', 166 | '3G0': 'Saab', 167 | '3G1': 'Chevrolet', 168 | '3G2': 'Pontiac', 169 | '3G5': 'Buick', 170 | '3G7': 'Pontiac', 171 | '3GC': 'Chevrolet Mexico', 172 | '3GD': 'GMC', 173 | '3GK': 'GMC', 174 | '3GN': 'Chevrolet Mexico', 175 | '3GS': 'Saturn', 176 | '3GT': 'GMC', 177 | '3GV': 'Chevrolet', 178 | '3GY': 'Cadillac', 179 | '3H': 'Honda', 180 | '3KP': 'Kia', 181 | '3LN': 'Lincoln', 182 | '3MD': 'Mazda', 183 | '3ME': 'Mercury Mexico', 184 | '3MY': 'Mazda Mexico', 185 | '3MZ': 'Mazda Mexico', 186 | '3N': Nissan(), 187 | '3N6': 'Chevrolet', 188 | '3P3': 'Plymouth Mexico', 189 | '3TM': 'Toyota Mexico', 190 | '3VV': 'Volkswagen', 191 | '3VW': 'Volkswagen', 192 | '460': 'Mercedes-Benz', 193 | '4A': 'Mitsubishi', 194 | '4B3': 'Dodge', 195 | '4C3': 'Chrysler', 196 | '4F': 'Mazda', 197 | '4G1': 'Chevrolet', 198 | '4GD': Opel(), 199 | '4J': 'Mercedes-Benz', 200 | '4M': 'Mercury', 201 | '4N2': Nissan(), 202 | '4NU': 'Isuzu', 203 | '4RK': 'Nova Bus', 204 | '4S': 'Subaru-Isuzu Automotive', 205 | '4S2': 'Isuzu', 206 | '4S3': 'Subaru', 207 | '4S4': 'Subaru', 208 | '4S6': 'Honda', 209 | '4T': 'Toyota', 210 | '4US': 'BMW', 211 | '4UZ': 'Frt-Thomas Bus', 212 | '4V': 'Volvo', 213 | '54D': 'Chevrolet', # Spartan, 214 | '55': 'Mercedes-Benz', 215 | '55S': 'Mercedes-Benz', 216 | '58A': 'Lexus', 217 | '5BZ': Nissan(), 218 | '5F': 'Honda', 219 | '5FR': 'Acura', 220 | '5GA': 'Buick', 221 | '5GN': 'Hummer', 222 | '5GR': 'Hummer', 223 | '5GT': 'Hummer', 224 | '5GZ': 'Saturn', 225 | '5HD': 'Harley-Davidson', 226 | '5J6': 'Honda', 227 | '5J8': 'Acura', 228 | '5KB': 'Honda', 229 | '5L': 'Lincoln', 230 | '5N1': Nissan(), 231 | '5N3': Nissan('Infiniti'), 232 | '5NA': Nissan(), 233 | '5NM': 'Hyundai', 234 | '5NP': 'Hyundai', 235 | '5S3': 'Saab', 236 | '5T': 'Toyota', 237 | '5U': 'BMW', 238 | '5X': 'Hyundai/Kia', 239 | '5XX': 'Kia', 240 | '5XY': 'Kia', 241 | '5Y2': 'Pontiac', 242 | '5YF': 'Toyota', 243 | '5YJ': 'Tesla', 244 | '5YM': 'BMW', 245 | '5Z6': 'Suzuki', 246 | '601': 'Replica/Kit Makes', 247 | '602': 'Toyota', 248 | '6AB': 'MAN', 249 | '6F': 'Ford', 250 | '6F4': Nissan('Nissan Motor Company'), 251 | '6F5': 'Kenworth', 252 | '6FP': 'Ford Motor Company', 253 | '6G': 'General Motors', 254 | '6G1': 'Chevrolet', 255 | '6G2': 'Pontiac', 256 | '6G3': 'Chevrolet Australia', 257 | '6H': 'Holden', 258 | '6H8': 'General Motors-Holden', 259 | '6MM': 'Mitsubishi', 260 | '6T1': 'Toyota', 261 | '7A3': 'Honda', 262 | '7FA': 'Honda', 263 | '8A1': Renault(), 264 | '8AC': 'Mercedes Benz', 265 | '8AD': 'Peugeot', 266 | '8AF': 'Ford', 267 | '8AG': 'General Motors', 268 | '8AJ': 'Toyota', 269 | '8AK': 'Suzuki', 270 | '8AP': 'Fiat', 271 | '8AT': 'Iveco', 272 | '8AW': 'Volkswagen', 273 | '8BC': 'Citroën', 274 | '8BR': 'Mercedes-Benz Argentina', 275 | '8BT': 'Mercedes-Benz Argentina', 276 | '8C3': 'Honda', 277 | '8GD': 'Peugeot', 278 | '8GG': 'Chevrolet', 279 | '92T': Bajaj(), 280 | '932': 'Harley-Davidson', 281 | '935': 'Citroën', 282 | '936': 'Peugeot', 283 | '93H': 'Honda', 284 | '93R': 'Toyota', 285 | '93U': 'Audi', 286 | '93V': 'Audi Brazil', 287 | '93W': 'Fiat Professional', 288 | '93X': 'Souza Ramos - Mitsubishi / Suzuki', 289 | '93Y': Renault(), 290 | '93Z': 'Iveco', 291 | '94D': Nissan(), 292 | '953': 'VW Trucks / MAN', 293 | '95P': 'CAOA / Hyundai', 294 | '95V': Dafra(), 295 | '96P': 'Kawasaki', 296 | '97N': 'Triumph', 297 | '988': 'Jeep', 298 | '98M': 'BMW', 299 | '98P': 'DAF Trucks', 300 | '98R': 'Chery', 301 | '99A': 'Audi', 302 | '99J': 'JLR Jaguar Land Rover', 303 | '99Z': 'BMW M', 304 | '9BD': 'Fiat Automóveis', 305 | '9BF': 'Ford', 306 | '9BG': 'General Motors', 307 | '9BH': 'Hyundai Motor Company / Hyundai', 308 | '9BM': 'Mercedes Benz', 309 | '9BR': 'Toyota', 310 | '9BS': 'Scania', 311 | '9BV': 'Volvo Trucks', 312 | '9BW': 'Volkswagen', 313 | '9C2': 'Honda Motorcycles', 314 | '9C6': 'Yamaha', 315 | '9CD': 'Suzuki Motorcycles', 316 | '9FB': Renault(), 317 | '9UJ': 'Chery', 318 | '9UK': 'Lifan', 319 | '9UW': 'Kia', 320 | 'AAV': 'Volkswagen', 321 | 'AFA': 'Ford', 322 | 'AHT': 'Toyota', 323 | 'B01': 'Cadillac', 324 | 'CF1': Renault(), 325 | 'CL9': 'Wallyscar', 326 | 'DA4': 'Jeep', 327 | 'EDB': 'Mercedes-Benz', 328 | 'FM2': 'Mercury', 329 | 'FSM': 'FSM', 330 | 'FV1': Renault(), 331 | 'FV3': 'Peugeot', 332 | 'FV7': 'Citroen', 333 | 'G4G': 'Buick', 334 | 'G61': 'Cadillac', 335 | 'GA1': Renault(), 336 | 'GKA': 'GMC', 337 | 'JA': 'Isuzu', 338 | 'JA3': 'Mitsubishi', 339 | 'JA4': 'Mitsubishi', 340 | 'JAC': 'Isuzu', 341 | 'JAE': 'Acura', 342 | 'JB3': 'Dodge', 343 | 'JC1': 'Fiat Automobiles/Mazda', 344 | 'JDA': 'Daihatsu', 345 | 'JF': 'Fuji Heavy Industries', 346 | 'JF1': 'Scion', 347 | 'JF2': 'Subaru', 348 | 'JF4': 'Saab', 349 | 'JFS': 'Subaru', 350 | 'JGN': 'Chevrolet', 351 | 'JH': 'Honda', 352 | 'JH4': 'Acura', 353 | 'JHM': 'Honda', 354 | 'JK': 'Kawasaki', 355 | 'JM': 'Mazda', 356 | 'JMB': 'Mitsubishi', 357 | 'JN': Nissan(), 358 | 'JNK': Nissan('Infiniti'), 359 | 'JNR': Nissan('Infiniti'), 360 | 'JNT': Nissan('Infiniti'), 361 | 'JNX': Nissan('Infiniti'), 362 | 'JS': 'Suzuki', 363 | 'JT': 'Toyota', 364 | 'JT6': 'Lexus', 365 | 'JT8': 'Lexus', 366 | 'JTD': 'Toyota', 367 | 'JTE': 'Toyota', 368 | 'JTH': 'Lexus', 369 | 'JTJ': 'Lexus', 370 | 'JTK': 'Scion', 371 | 'JTL': 'Scion', 372 | 'JTM': 'Toyota', 373 | 'JTN': 'Scion', 374 | 'JTS': 'Toyota', 375 | 'JY': 'Yamaha', 376 | 'KC4': 'Dodge', 377 | 'KL': 'Daewoo/GM Korea', 378 | 'KL1': 'Chevrolet', 379 | 'KL2': 'Pontiac', 380 | 'KL4': 'Buick', 381 | 'KL5': 'Suzuki', 382 | 'KL7': 'Chevrolet', 383 | 'KL8': 'Chevrolet', 384 | 'KLA': 'Chevrolet', 385 | 'KLM': 'Lincoln', 386 | 'KM': 'Hyundai', 387 | 'KMH': 'Genesis', 388 | 'KN': 'Kia', 389 | 'KN1': Nissan(), 390 | 'KNM': Renault('Renault Samsung'), 391 | 'KP': 'SsangYong', 392 | 'KRX': 'BMW', 393 | 'L4C': 'Buick', 394 | 'L56': Renault('Renault Samsung'), 395 | 'L5Y': 'Merato Motorcycle Taizhou Zhongneng', 396 | 'L6T': 'Geely', 397 | 'LBE': 'Beijing Hyundai', 398 | 'LBV': 'BMW Brilliance', 399 | 'LC0': 'BYD Bus', 400 | 'LDC': 'Dongfeng Peugeot-Citroën', 401 | 'LDY': 'Zhongtong Coach', 402 | 'LE4': 'Beijing Benz', 403 | 'LFM': 'FAW Toyota', 404 | 'LFP': 'FAW Car', 405 | 'LFV': 'FAW-Volkswagen', 406 | 'LGB': 'Dongfeng Nissan', 407 | 'LGH': 'Dong Feng (DFM), China', 408 | 'LGJ': 'Dongfeng Fengshen', 409 | 'LGW': 'Great Wall (Havel)', 410 | 'LGX': 'BYD Auto', 411 | 'LH1': 'FAW Haima', 412 | 'LHG': 'Guangzhou Honda', 413 | 'LJ1': 'JAC', 414 | 'LJD': 'Dongfeng Yueda Kia', 415 | 'LKL': 'Suzhou King Long', 416 | 'LLV': 'Lifan', 417 | 'LMG': 'GAC Trumpchi', 418 | 'LPA': 'Changan PSA (DS Automobiles)', 419 | 'LRB': 'Buick China', 420 | 'LS5': 'Changan Suzuki', 421 | 'LSG': 'SAIC General Motors', 422 | 'LSJ': 'SAIC MG', 423 | 'LSV': 'SAIC Volkswagen', 424 | 'LSY': 'Brilliance Zhonghua', 425 | 'LTV': 'FAW Toyota (Tianjin)', 426 | 'LUC': 'Honda', 427 | 'LVG': 'GAC Toyota', 428 | 'LVH': 'Dongfeng Honda', 429 | 'LVR': 'Changan Mazda', 430 | 'LVS': 'Changan Ford', 431 | 'LVV': 'Chery', 432 | 'LVY': 'Volvo', 433 | 'LWV': 'GAC Fiat', 434 | 'LYV': 'Volvo China', 435 | 'LZE': 'Isuzu Guangzhou', 436 | 'LZG': 'Shaanxi Automobile Group', 437 | 'LZM': 'MAN', 438 | 'LZW': 'SAIC GM Wuling', 439 | 'LZY': 'Yutong', 440 | 'MA1': 'Mahindra', 441 | 'MA3': 'Suzuki', 442 | 'MA7': 'Honda Siel Cars', 443 | 'MAJ': 'FordS', 444 | 'MAL': 'Hyundai', 445 | 'MAT': 'Tata', 446 | 'MBH': Nissan(), 447 | 'MC2': 'Volvo Eicher commercial vehicles limited.', 448 | 'MD2': Bajaj(), 449 | 'MDH': Nissan(), 450 | 'MHR': 'Honda', 451 | 'ML3': 'Mitsubishi Thailand', 452 | 'MM0': 'Mazda', 453 | 'MM8': 'Mazda', 454 | 'MMB': 'Mitsubishi', 455 | 'MMC': 'Mitsubishi', 456 | 'MMM': 'Chevrolet', 457 | 'MMS': 'Suzuki', 458 | 'MMT': 'Mitsubishi', 459 | 'MNB': 'Ford', 460 | 'MNT': Nissan(), 461 | 'MP1': 'Isuzu', 462 | 'MPA': 'Isuzu', 463 | 'MR0': 'Toyota', 464 | 'MRH': 'Honda', 465 | 'MS0': 'KIA Myanmar', 466 | 'NLA': 'Honda', 467 | 'NLE': 'Mercedes-Benz Turk Truck', 468 | 'NLH': 'Hyundai', 469 | 'NLJ': 'Hyundai', 470 | 'NM0': 'Ford Otosan', 471 | 'NM4': 'Tofas Turk', 472 | 'NMT': 'Toyota', 473 | 'PE1': 'Ford', 474 | 'PE3': 'Mazda', 475 | 'PL1': 'Proton', 476 | 'SAD': 'Jaguar', 477 | 'SAH': 'Honda', 478 | 'SAJ': 'Jaguar', 479 | 'SAL': 'Land Rover', 480 | 'SAR': 'Rover', 481 | 'SAT': 'Triumph', 482 | 'SAX': 'Rover', 483 | 'SB1': 'Toyota', 484 | 'SBM': 'Mclaren', 485 | 'SCA': 'Rolls Royce', 486 | 'SCB': 'Bentley', 487 | 'SCC': 'Lotus Cars', 488 | 'SCE': 'DeLorean', 489 | 'SCF': 'Aston Martin Lagonda Limited', 490 | 'SDB': 'Peugeot UK', 491 | 'SED': Opel(), 492 | 'SEY': 'LDV', 493 | 'SFA': 'Ford', 494 | 'SFD': 'Alexander Dennis', 495 | 'SHH': 'Honda', 496 | 'SHS': 'Honda', 497 | 'SJA': 'Bentley', 498 | 'SJK': Nissan('Infiniti'), 499 | 'SJN': Nissan(), 500 | 'SKF': Opel(), 501 | 'SMT': 'Triumph', 502 | 'SNE': 'Jeep', 503 | 'SNT': 'Honda', 504 | 'STE': 'Toyota', 505 | 'SU9': 'Solaris Bus & Coach', 506 | 'SUF': 'Fiat Auto Poland / FSM', 507 | 'SUL': 'Daewoo Poland / FSO', 508 | 'SUP': 'Daewoo Poland / FSO', 509 | 'SUR': 'Land Rover', 510 | 'TC3': 'Chrysler', 511 | 'TCC': 'Micro Compact Car AG (SMART 1998-1999)', 512 | 'TDM': 'QUANTYA Swiss Electric Movement', 513 | 'TK9': 'SOR', 514 | 'TM9': 'Škoda trolleybuses', 515 | 'TMA': 'Hyundai', 516 | 'TMB': 'Škoda', 517 | 'TMK': 'Karosa', 518 | 'TMP': 'Škoda trolleybuses', 519 | 'TMT': 'Tatra', 520 | 'TN9': 'Karosa', 521 | 'TNB': 'Skoda', 522 | 'TRA': 'Ikarus Bus', 523 | 'TRU': 'Audi', 524 | 'TSE': 'Ikarus Egyedi Autobuszgyar', 525 | 'TSM': 'Suzuki', 526 | 'TYB': 'Mitsubishi', 527 | 'U5Y': 'Kia', 528 | 'U6Y': 'Kia', 529 | 'USY': 'Kia', 530 | 'UU': 'Dacia', 531 | 'UU1': Renault('Renault Dacia'), 532 | 'V0L': Opel(), 533 | 'VA0': 'ÖAF', 534 | 'VBK': 'KTM', 535 | 'VF0': 'Ford', 536 | 'VF1': Renault(), 537 | 'VF2': Renault(), 538 | 'VF3': 'Peugeot', 539 | 'VF4': 'Talbot', 540 | 'VF5': 'Iveco Unic SA', 541 | 'VF6': 'Renault Trucks/Volvo', 542 | 'VF7': 'Citroën', 543 | 'VF8': 'Matra/Talbot/Simca', 544 | 'VF9': 'Bugatti', 545 | 'VFB': Renault(), 546 | 'VFE': 'IvecoBus', 547 | 'VFF': 'Peugeot', 548 | 'VFG': 'Citroen', 549 | 'VFJ': Renault(), 550 | 'VFZ': 'Citroen', 551 | 'VH8': 'Microcar', 552 | 'VLG': 'Aixam', 553 | 'VLU': 'Scania', 554 | 'VN1': Opel(), 555 | 'VNE': 'Irisbus', 556 | 'VNK': 'Toyota', 557 | 'VNV': Renault(), 558 | 'VR7': 'Citroën', 559 | 'VS1': 'Iveco', 560 | 'VS3': 'Peugeot', 561 | 'VS5': Renault(), 562 | 'VS6': 'Ford', 563 | 'VS7': 'Citroen', 564 | 'VS9': 'Carrocerias Ayats', 565 | 'VSA': 'Mercedes-Benz', 566 | 'VSE': 'Suzuki / Santana Motors', 567 | 'VSK': Nissan(), 568 | 'VSS': 'SEAT', 569 | 'VSX': Opel(), 570 | 'VSY': Renault(), 571 | 'VSZ': 'Seat', 572 | 'VV9': 'Tauro Sport Auto', 573 | 'VW1': Renault(), 574 | 'VW2': 'Volkswagen', 575 | 'VWA': Nissan(), 576 | 'VWG': 'Volkswagen Spain', 577 | 'VWV': 'Volkswagen', 578 | 'VX1': 'Zastava / Yugo', 579 | 'VY1': 'Volvo', 580 | 'W00': Opel(), 581 | 'W04': 'Buick', 582 | 'W06': 'Cadillac', 583 | 'W08': 'Saturn', 584 | 'W09': 'Ruf Automobile', 585 | 'W0L': Opel('Opel/Vauxhall'), 586 | 'W0S': Opel('Opel Special Vehicles'), 587 | 'W0V': Opel(), 588 | 'W1': 'Mercedes-Benz', 589 | 'WA1': 'Audi', 590 | 'WAG': 'Neoplan', 591 | 'WAP': 'Alpina', 592 | 'WAU': 'Audi', 593 | 'WAV': 'Audi', 594 | 'WAX': 'SsangYong', 595 | 'WB': 'BMW', 596 | 'WBA': 'BMW', 597 | 'WBD': 'Mercedes-Benz', 598 | 'WBS': 'BMW M', 599 | 'WBX': 'BMW', 600 | 'WBY': 'BMW', 601 | 'WCD': 'Mercedes-Benz (Sprinter)', 602 | 'WD0': 'Dodge', 603 | 'WD2': 'Dodge', 604 | 'WD3': 'Daimler AG (Sprinter)', 605 | 'WD4': 'Daimler AG (Sprinter)', 606 | 'WD5': 'Dodge', 607 | 'WD8': 'Mercedes-Benz', 608 | 'WDA': 'Daimler AG (Sprinter)', 609 | 'WDB': 'Mercedes-Benz', 610 | 'WDC': 'DaimlerChrysler AG/Daimler AG', 611 | 'WDD': 'DaimlerChrysler AG/Daimler AG', 612 | 'WDF': 'Mercedes-Benz', 613 | 'WDP': 'Mercedes-Benz (Sprinter)', 614 | 'WDR': 'Mercedes-Benz (Sprinter)', 615 | 'WDW': 'Dodge', 616 | 'WDX': 'Dodge', 617 | 'WDY': 'Mercedes-Benz (Sprinter)', 618 | 'WDZ': 'Mercedes-Benz (Sprinter)', 619 | 'WE0': 'Ford', 620 | 'WEB': 'EvoBus', 621 | 'WF0': 'Ford of Europe', 622 | 'WF1': Renault(), 623 | 'WF3': 'Peugeot', 624 | 'WF7': 'Citroen', 625 | 'WFD': 'Fliegl', 626 | 'WFO': 'Ford', 627 | 'WJM': 'Iveco', 628 | 'WJR': 'Irmscher', 629 | 'WKK': 'Karl Kässbohrer Fahrzeugwerke', 630 | 'WMA': 'MAN', 631 | 'WMB': 'Audi', 632 | 'WME': 'Smart', 633 | 'WMW': 'Mini', 634 | 'WMX': 'DaimlerChrysler AG/Daimler AG', 635 | 'WMZ': 'MINI', 636 | 'WNK': 'Toyota', 637 | 'WOL': Opel(), 638 | 'WP0': 'Porsche car', 639 | 'WP1': 'Porsche SUV', 640 | 'WS0': 'Ford', 641 | 'WSS': 'Seat', 642 | 'WUA': 'Quattro', 643 | 'WUW': 'Volkswagen', 644 | 'WV': 'Volkswagen', 645 | 'WV0': 'Ford', 646 | 'WV1': 'Volkswagen Commercial Vehicles', 647 | 'WV2': 'Volkswagen Commercial Vehicles', 648 | 'WV3': 'Volkswagen Trucks', 649 | 'WVG': 'Volkswagen', 650 | 'WVW': 'Volkswagen', 651 | 'WVZ': 'Volkswagen', 652 | 'WWD': 'Mercedes-Benz', 653 | 'WWW': 'Volkswagen', 654 | 'WYG': 'Volkswagen', 655 | 'WYW': 'Volkswagen', 656 | 'WZW': 'Volkswagen', 657 | 'X7L': Renault(), 658 | 'X96': 'Mercedes-Benz', 659 | 'XL9': 'Spyker', 660 | 'XLB': 'Volvo', 661 | 'XLR': 'DAF Trucks', 662 | 'XMC': 'Mitsubishi (NedCar)', 663 | 'XNC': 'Mitsubishi', 664 | 'XTA': Lada('AvtoVAZ'), 665 | 'XUF': Opel(), 666 | 'XW8': 'Volkswagen', 667 | 'XWE': 'Kia', 668 | 'XWF': Opel(), 669 | 'XXX': 'Honda', 670 | 'Y6D': Opel(), 671 | 'YAR': 'Toyota', 672 | 'YCM': 'Mazda', 673 | 'YH4': 'Fisker', 674 | 'YK1': 'Saab', 675 | 'YMB': 'Skoda', 676 | 'YS2': 'Scania, Södertälje', 677 | 'YS3': 'Saab', 678 | 'YS4': 'Scania, Katrineholm', 679 | 'YTN': 'Saab NEVS', 680 | 'YV1': 'Volvo Cars', 681 | 'YV2': 'Volvo Trucks', 682 | 'YV3': 'Volvo Buses', 683 | 'YV4': 'Volvo Cars', 684 | 'Z12': Opel(), 685 | 'Z3B': 'Chevrolet', 686 | 'ZA9': 'Bugatti', 687 | 'ZAC': 'FCA', 688 | 'ZAF': 'Fiat', 689 | 'ZAM': 'Maserati', 690 | 'ZAP': 'Piaggio/Vespa/Gilera', 691 | 'ZAR': 'Alfa Romeo', 692 | 'ZAS': 'Alfa', 693 | 'ZCF': 'Iveco', 694 | 'ZCG': 'Cagiva SpA', 695 | 'ZD4': 'Aprilia', 696 | 'ZDF': 'Ferrari Dino', 697 | 'ZDM': 'Ducati Motor Holdings SpA', 698 | 'ZFA': 'Fiat Automobiles', 699 | 'ZFB': 'Fiat', 700 | 'ZFC': 'Fiat V.I.', 701 | 'ZFF': 'Ferrari', 702 | 'ZGA': 'IvecoBus', 703 | 'ZHW': 'Lamborghini', 704 | 'ZLA': 'Lancia', 705 | 'ZN6': 'Maserati', 706 | 'ZOM': 'OM', 707 | 'ZSA': 'Fiat', 708 | } 709 | --------------------------------------------------------------------------------