# Parking Lot ## ### Requirements
### Class Diagram
### Sequence Diagram - **Vehicle Entry**
### Code - **Vehicle** ```python """Module: Vehicle and vehicle types.""" from abc import ABC, abstractmethod from enum import Enum class VehicleType(str, Enum): """Types of vehicles.""" CAR = "car" TRUCK = "truck" MOTORBIKE = "motorbike" class Vehicle: """Class: Vehicle.""" def __init__(self, vehicle_id: int, vehicle_type: VehicleType): """Initializes Vehicle instance Args: vehicle_id (int): Unique identifier of vehicle vehicle_type (Enum): Vehicle type Enum """ self.vehicle_id = vehicle_id self.vehicle_type = vehicle_type self.ticket = None def __str__(self): class_name = type(self).__name__ return f"{class_name}(vehicle_id={self.vehicle_id}, vehicle_type={self.vehicle_type})" class Car(Vehicle): """Class: Car.""" def __init__(self, vehicle_id: int): super().__init__(vehicle_id, VehicleType.CAR) class Truck(Vehicle): """Class: Truck.""" def __init__(self, vehicle_id: int): super().__init__(vehicle_id, VehicleType.TRUCK) class Motorbike(Vehicle): """Class: Motorbike.""" def __init__(self, vehicle_id: int): super().__init__(vehicle_id, VehicleType.MOTORBIKE) ``` - **Parking Spot** ```python """Module: Parking spot.""" from enum import Enum from vehicle import Vehicle class ParkingSpotType(Enum): """Types of Parking Spots.""" HANDICAPPED = "handicapped" COMPACT = "compact" LARGE = "large" MOTORBIKE = "motorbike" class ParkingSpot: """Class: Parking Spot.""" def __init__(self, floor: int, spot_id: int, spot_type: ParkingSpotType): """Initialize Vehicle instance. Args: floor (int): Floor number spot_id (int): Parking spot number parking_spot_type (Enum): Parking spot type Enum """ self._floor = floor self.spot_id = spot_id self._free = True self._vehicle = None self.spot_type = spot_type def assign_vehicle(self, vehicle: Vehicle): """Assign vehicle to parking spot. Args: vehicle (Vehicle): Vehicle Instance """ self._vehicle = vehicle self._free = False def remove_vehicle(self): """Remove vehicle from parking spot.""" self._vehicle = None self._free = True class HandicappedSpot(ParkingSpot): """Class: Handicapped Parking Spot.""" def __init__(self, number: int): """Initialize handicapped parking spot.""" super().__init__(number, ParkingSpotType.HANDICAPPED) class CompactSpot(ParkingSpot): """Class: Handicapped Parking Spot.""" def __init__(self, number: int): """Initialize handicapped parking spot.""" super().__init__(number, ParkingSpotType.COMPACT) class LargeSpot(ParkingSpot): """Class: Large Parking Spot.""" def __init__(self, number: int): """Initialize large parking spot.""" super().__init__(number, ParkingSpotType.LARGE) class MotorbikeSpot(ParkingSpot): """Class: Motorbike Parking Spot.""" def __init__(self, number: int): """Initialize motorbike parking spot.""" super().__init__(number, ParkingSpotType.MOTORBIKE) ``` - **Parking Ticket** ```python """Module: Parking Ticket.""" from datetime import datetime from enum import Enum from uuid import UUID, uuid4 import vehicle from parking_spot import ParkingSpotType from pydantic import BaseModel class ParkingTicketStatus(Enum): """Enum: ParkingTicketStatus.""" UNPAID = "unpaid" PAID = "paid" LOST = "lost" class ParkingTicket(BaseModel): """Class: Parking Ticket.""" ticket_id: UUID = uuid4() entrance_id: int spot_id: int spot_type: ParkingSpotType vehicle_id: int vehicle_type: vehicle.VehicleType issued_at: datetime paid_at: datetime | None exit_id: int | None status: ParkingTicketStatus paid_amount: float | None ``` - **Entrance, Exit Panels** ```python """Module: Entrance, Exit Panels.""" import logging.config from datetime import datetime from pathlib import Path from uuid import uuid4 import yaml from parking_spot import ParkingSpot from parking_ticket import ParkingTicket, ParkingTicketStatus from vehicle import Vehicle logger = logging.getLogger(__name__) class EntrancePanel: """Class: Entrance Panel.""" def __init__(self, panel_id: int): """Initialize entrance panel instance.""" self._panel_id = panel_id def issue_ticket( self, vehicle: Vehicle, parking_spot: ParkingSpot ) -> ParkingTicket: """Issue ticket to vehicle.""" parking_ticket = ParkingTicket( ticket_id=uuid4(), entrance_id=self._panel_id, spot_id=parking_spot.spot_id, spot_type=parking_spot.spot_type, vehicle_id=vehicle.vehicle_id, vehicle_type=vehicle.vehicle_type, issued_at=datetime.now(), paid_at=None, exit_id=None, status=ParkingTicketStatus.UNPAID, paid_amount=None, ) return parking_ticket class ExitPanel: """Class: Exit Panel.""" def __init__(self, panel_id: int): """Initialize exit panel instance.""" self._panel_id = panel_id def scan_ticket(self, ticket: ParkingTicket, rates): """Scan ticket at exit.""" current_timestamp = datetime.now() seconds_elapsed = (current_timestamp - ticket.issued_at).seconds total_amount = rates[ticket.spot_type] * seconds_elapsed # Accept payment and update ticket payment status ticket.paid_at = current_timestamp ticket.exit_id = self._panel_id ticket.status = ParkingTicketStatus.PAID ticket.paid_amount = total_amount return ticket class DisplayBoard: """Class:Display board.""" def __init__(self, board_id: int): """Initialize display board instance.""" self._board_id = board_id def update_num_free_spot_counts(self, num_free_spots): """Update count of free spots.""" logger.info(f"DisplayBoard{self._board_id}: ") for spot_type, free_spots in num_free_spots.items(): logger.info(f"{spot_type}: {free_spots} free spots available.") ``` - **Parking Lot System** ```python """Module: Parking Lot.""" import logging.config import threading import time from collections import defaultdict from concurrent import futures from pathlib import Path import yaml from panel import DisplayBoard, EntrancePanel, ExitPanel from parking_spot import ParkingSpot, ParkingSpotType from parking_spot_strategy import FindNearestSpotStrategy, FindRandomSpotStrategy from parking_ticket import ParkingTicket from vehicle import Vehicle logging.config.dictConfig( yaml.safe_load(Path("src/logs/logging_config.yaml").read_text()) ) logger = logging.getLogger(__name__) class ParkingLot: """Class: Parking Lot.""" def __init__( self, num_entrance_panels, num_exit_panels, num_display_boards, parking_spot_counts, parking_spot_rates_per_sec, vehicle_spot_type_mapping, find_parking_spot_strategy, ): """Initialize Parking Lot instance.""" self._entrance_panels = {} self._exit_panels = {} self._display_boards = {} # Add entrance panels, exit panels, display boards self.add_entrance_panels(num_entrance_panels) self.add_exit_panels(num_exit_panels) self.add_display_boards(num_display_boards) # Add parking spots self._spots_free = defaultdict() self._spots_occupied = defaultdict() self._num_free_spots = defaultdict(int) self.add_parking_spots(parking_spot_counts) self._vehicle_spot_type_mapping = vehicle_spot_type_mapping self._rates_per_sec = parking_spot_rates_per_sec # Store all tickets for downstream analytics self._tickets = defaultdict(ParkingTicket) self._lock = threading.Lock() # Initializing strategis is done in a separate thread self._parking_spot_counts = parking_spot_counts self._find_parking_spot_strategy = find_parking_spot_strategy self._init_find_parking_spot_strategies() logger.info("***** Initialize Parking Lot with Settings *****") logger.info(f" Number of entrance panels: {len(self._entrance_panels)}") logger.info(f" Number of exit panels: {len(self._exit_panels)}") logger.info(f" Number of display boards: {len(self._display_boards)}") for spot_type, num_spots in self._num_free_spots.items(): logger.info(f"{spot_type}: {num_spots} total spots available.") logger.info(f" Find parking spot strategy: {self._find_parking_spot_strategy}") for spot_type, spot_rate in self._rates_per_sec.items(): logger.info(f"{spot_type}: {spot_rate} unit per sec.") logger.info("************************************************") def _init_find_parking_spot_strategies(self): with futures.ThreadPoolExecutor(max_workers=4) as executor: futures_map = {} # Map futures_map["first"] = executor.submit(FindRandomSpotStrategy) futures_map["nearest"] = executor.submit( FindNearestSpotStrategy, self._entrance_panels, self._spots_free, self._parking_spot_counts, ) futures.as_completed(futures_map) self._find_parking_spot_strategies = {} # Map # Iterate over futures as result becomes available for strategy, future in futures_map.items(): self._find_parking_spot_strategies[strategy] = future.result() self._find_parking_spot_strategy = self._find_parking_spot_strategies[ self._find_parking_spot_strategy ] return def add_parking_spots(self, parking_spot_counts: dict[ParkingSpotType, int]): """Add parking spots of different types.""" acc_num_spots = 0 for spot_type, num_spots in parking_spot_counts.items(): self._spots_free[spot_type] = {} for i in range(num_spots): spot_id = acc_num_spots + i self._spots_free[spot_type][spot_id] = ParkingSpot( floor=0, spot_id=spot_id, spot_type=spot_type ) self._spots_occupied[spot_type] = {} self._num_free_spots[spot_type] = num_spots acc_num_spots += num_spots def add_entrance_panels(self, num_entrance_panels: int): """Add entrance panels.""" for i in range(num_entrance_panels): self._entrance_panels[i] = EntrancePanel(panel_id=i) def add_exit_panels(self, num_exit_panels: int): """Add exit panel.""" for i in range(num_exit_panels): self._exit_panels[i] = ExitPanel(panel_id=i) def add_display_boards(self, num_display_boards: int): """Add display boards.""" for i in range(num_display_boards): self._display_boards[i] = DisplayBoard(board_id=i) def notify_display_boards(self): """Update display boards with number of free spot counts.""" for i in range(len(self._display_boards)): self._display_boards[i].update_num_free_spot_counts(self._num_free_spots) def get_parking_spot( self, entrance_panel_id: int, spot_type: ParkingSpotType, vehicle: Vehicle ) -> None | ParkingSpot: """Find parking spot Args: entrance_panel_id (int): Unique ID of entrance panel spot_type (Enum): ParkingSpotType vehicle (Vehicle): Instance of vehicle class Returns: parking_spot (None | ParkingSpot) """ parking_spot = None # Acquire lock with self._lock: # If parking spots for this vehicle type is full, return None (no ticket assigned) if not self._num_free_spots[spot_type]: logger.info(f"Parking Spots for {vehicle.vehicle_type} are full") return parking_spot """Get parking spot.""" spot_id = self._find_parking_spot_strategy.find_parking_spot( entrance_panel_id, spot_type, self._spots_free[spot_type] ) # Get the parking spot for this spot_id parking_spot = self._spots_free[spot_type][spot_id] # Assign vehicle to this spot parking_spot.assign_vehicle(vehicle=vehicle) # Remove this spot from free spots and add it to occupied spots self._spots_free[spot_type].pop(spot_id) self._spots_occupied[spot_type][spot_id] = parking_spot self._num_free_spots[spot_type] -= 1 # Release lock return parking_spot def handle_vehicle_entrance( self, entrance_panel_id: int, vehicle: Vehicle ) -> ParkingTicket | None: logger.info( f"Vehicle: Type: {vehicle.vehicle_type}, Vehicle ID: {vehicle.vehicle_id} at entrance panel id:{entrance_panel_id}" ) """Handle vehicle at entrance panel.""" if entrance_panel_id >= len(self._entrance_panels): raise ValueError("entrance_panel_id is out of bounds") # Get the mapping to appropriate parking spot type for this vehicle spot_type = self._vehicle_spot_type_mapping[vehicle.vehicle_type] # Get parking spot for current vehicle parking_spot = self.get_parking_spot(entrance_panel_id, spot_type, vehicle) # If parking spots for this vehicle type is full, return None (no ticket assigned) if not parking_spot: return None logger.info(f"Assigned {spot_type} with id:{parking_spot.spot_id}") # Issue ticket parking_ticket = self._entrance_panels[entrance_panel_id].issue_ticket( vehicle=vehicle, parking_spot=parking_spot ) # Assign ticket to vehicle vehicle.ticket = parking_ticket logger.info( f"Ticket assigned, ID:{parking_ticket.ticket_id} at {parking_ticket.issued_at}" ) # Updating display boards with latest counts self.notify_display_boards() return parking_ticket def handle_vehicle_exit( self, exit_panel_id: int, vehicle: Vehicle, ): """Handle vehicle's exit Scan Ticket Accept Payment. """ if exit_panel_id >= len(self._exit_panels): raise ValueError("exit_panel_id is out of bounds") # Scan ticket and handle payment ticket = self._exit_panels[exit_panel_id].scan_ticket( ticket=vehicle.ticket, rates=self._rates_per_sec ) # Save ticket (in DB) for downstream analytics self._tickets[ticket.ticket_id] = ticket logger.info( f"Vehicle: Type: {ticket.vehicle_type}, Vehicle ID: {ticket.vehicle_id} at exit panel id:{exit_panel_id}" ) # Acquire lock with self._lock: # Remove this spot from occupied spots and add it to free spots spot_id = ticket.spot_id spot_type = ticket.spot_type parking_spot = self._spots_occupied[spot_type][spot_id] self._spots_occupied[spot_type].pop(spot_id) self._spots_free[spot_type][spot_id] = parking_spot self._num_free_spots[spot_type] += 1 self.notify_display_boards() # Update list of free spots in find parking spot strategies self._find_parking_spot_strategy.update_parking_spot(spot_id, spot_type) # Release lock logger.info(f"Spot freed: {ticket.spot_type} with id:{ticket.spot_id}") logger.info( f"Ticket scanned, ID:{ticket.ticket_id}. Payment of {ticket.paid_amount} handled at {ticket.paid_at}" ) return ``` - **Parking Lot Application** ```python """Module: Parking Lot Application.""" import logging.config import time from concurrent import futures from pathlib import Path from threading import Thread import typer import yaml # from account import AccountStatus, Admin, Person from panel import EntrancePanel, ExitPanel from parking_lot import ParkingLot from parking_spot import ParkingSpotType from typing_extensions import Annotated from vehicle import Car, Vehicle, VehicleType app = typer.Typer() logger = logging.getLogger(__name__) @app.command() def parking_lot_app( num_entrance_panels: Annotated[ int, typer.Argument(help="Number of entrance panels") ] = 2, num_exit_panels: Annotated[ int, typer.Argument(help="Number of entrance panels") ] = 2, num_display_boards: Annotated[ int, typer.Argument(help="Number of dispaly boards") ] = 1, find_parking_spot_strategy: Annotated[ str, typer.Argument( help="first: Find first free spot, nearest: Find nearest free spot to entrance" ), ] = "nearest", ): """ Initialize parking lot app. Args: num_entrance_panels (int): Number of entrance panels num_exit_panels (int): Number of exit panels """ parking_spot_counts = { ParkingSpotType.MOTORBIKE: 50, ParkingSpotType.COMPACT: 25, ParkingSpotType.LARGE: 15, ParkingSpotType.HANDICAPPED: 5, } parking_spot_rates_per_sec = { ParkingSpotType.MOTORBIKE: 0.0025, ParkingSpotType.COMPACT: 0.005, ParkingSpotType.LARGE: 0.01, ParkingSpotType.HANDICAPPED: 0.002, } vehicle_spot_type_mapping = { VehicleType.CAR: ParkingSpotType.COMPACT, VehicleType.TRUCK: ParkingSpotType.LARGE, VehicleType.MOTORBIKE: ParkingSpotType.MOTORBIKE, } # Create singleton instance of Parking Lot parking_lot = ParkingLot( num_entrance_panels, num_exit_panels, num_display_boards, parking_spot_counts, parking_spot_rates_per_sec, vehicle_spot_type_mapping, find_parking_spot_strategy, ) # Create vehicles car1 = Car(vehicle_id=1) car2 = Car(vehicle_id=2) def park_one_vehicle(args): entrance_panel_id, vehicle = args vehicle_id = vehicle.vehicle_id vechicle_type = vehicle.vehicle_type logger.info( f" Vehicle of type: {vechicle_type} with ID: {vehicle_id} arrived at entrance panel with id: {entrance_panel_id} " ) parking_lot.handle_vehicle_entrance( entrance_panel_id=entrance_panel_id, vehicle=vehicle ) def exit_one_vehicle(args): exit_panel_id, vehicle = args vehicle_id = vehicle.vehicle_id vechicle_type = vehicle.vehicle_type logger.info( f" Vehicle of type: {vechicle_type} with ID: {vehicle_id} exiting at exit panel with id: {exit_panel_id} " ) parking_lot.handle_vehicle_exit(exit_panel_id=exit_panel_id, vehicle=vehicle) with futures.ThreadPoolExecutor() as executor: _ = executor.map(park_one_vehicle, [(0, car1), (1, car2)]) time.sleep(3) _ = executor.map(exit_one_vehicle, [(0, car1), (1, car2)]) if __name__ == "__main__": app() ``` ### Unit Tests - **Fixtures** ```python """Test Parking lot's vehicle entry and exit scenarios.""" import logging.config import time from concurrent import futures import pytest from parking_lot import ParkingLot from parking_spot import ParkingSpotType from vehicle import Car, CarFactory, Vehicle, VehicleType logger = logging.getLogger(__name__) @pytest.fixture(scope="class") def parking_spot_counts(): def _parking_spot_counts(num_spots): return { ParkingSpotType.COMPACT: num_spots, ParkingSpotType.MOTORBIKE: 50, ParkingSpotType.LARGE: 15, ParkingSpotType.HANDICAPPED: 5, } return _parking_spot_counts @pytest.fixture(scope="class") def parking_spot_rates_per_sec(): return { ParkingSpotType.MOTORBIKE: 0.0025, ParkingSpotType.COMPACT: 0.005, ParkingSpotType.LARGE: 0.01, ParkingSpotType.HANDICAPPED: 0.002, } @pytest.fixture(scope="class") def vehicle_spot_type_mapping(): return { VehicleType.CAR: ParkingSpotType.COMPACT, VehicleType.TRUCK: ParkingSpotType.LARGE, VehicleType.MOTORBIKE: ParkingSpotType.MOTORBIKE, } @pytest.fixture(scope="class") def num_spots(request): return request.param @pytest.fixture(scope="class") def num_vehicles(request): return request.param @pytest.fixture(scope="class") def factory_parking_lot( parking_spot_counts, parking_spot_rates_per_sec, vehicle_spot_type_mapping, ): def _parking_lot(num_spots): num_entrance_panels = 2 num_exit_panels = 2 num_display_boards = 1 find_parking_spot_strategy = "nearest" return ParkingLot( num_entrance_panels, num_exit_panels, num_display_boards, parking_spot_counts(num_spots), parking_spot_rates_per_sec, vehicle_spot_type_mapping, find_parking_spot_strategy, ) return _parking_lot @pytest.fixture(scope="class") def factory_vehicles(): # Factory pattern to create instances of vehicles def _vehicles(num_vehicles): return [CarFactory().factory_method(vid) for vid in range(num_vehicles)] return _vehicles @pytest.fixture(scope="class") def parking_lot(num_spots, factory_parking_lot): return factory_parking_lot(num_spots) @pytest.fixture(scope="class") def vehicles(num_vehicles, factory_vehicles): return factory_vehicles(num_vehicles) @pytest.fixture(scope="class") def park_vehicles(): def _park_vehicles(parking_lot: ParkingLot, vehicles: list[Vehicle]): def park_one_vehicle(args): entrance_panel_id, vehicle = args parking_lot.handle_vehicle_entrance( entrance_panel_id=entrance_panel_id, vehicle=vehicle ) vehicles_entrance_inputs = [] for i, vehicle in enumerate(vehicles): entrance_panel_id = i % 2 vehicles_entrance_inputs.append((entrance_panel_id, vehicle)) with futures.ThreadPoolExecutor() as executor: _ = executor.map(park_one_vehicle, vehicles_entrance_inputs) return _park_vehicles ``` - **Unit Test 1: Spot Available Vehicle entry granted** ```python @pytest.mark.parametrize("num_vehicles, num_spots", [(1, 1), (2, 2)], indirect=True) class TestOneVehicleSpotAvailable: """Tests: 1. Ticket issued to vehicle entry with spot available. 2. Number of free spots reduces by 1. """ def test_vehicle_ticket_issued(self, parking_lot, vehicles, park_vehicles): """Unit test to verify vehicle entry when spot is available""" park_vehicles(parking_lot, vehicles) assert vehicles[0].ticket is not None def test_num_free_spots_after_vehicle_entrance( self, num_vehicles, num_spots, parking_lot ): """Unit Test to verify number of free spots is updated after vehicle's entry.""" spot_type = ParkingSpotType.COMPACT assert parking_lot._num_free_spots[spot_type] == num_spots - num_vehicles ``` - **Unit Test 2: No Spot Available Vehicle entry denied** ```python @pytest.mark.parametrize("num_vehicles, num_spots", [(1, 0)], indirect=True) class TestOneVehicleNoSpotAvailable: """Test Case: to verify vehicle entry denial if no spot is available""" def test_vehicle_entry_denied(self, parking_lot, vehicles, park_vehicles): """Unit test to verify vehicle entry is denied when no spot is available""" park_vehicles(parking_lot, vehicles) assert vehicles[0].ticket is None def test_num_free_spots_after_vehicle_entrance( self, num_vehicles, num_spots, parking_lot ): """Unit Test to verify number of free spots is updated after vehicle's entry.""" spot_type = ParkingSpotType.COMPACT assert parking_lot._num_free_spots[spot_type] == num_spots ``` - **Unit Test 3: Spot Available Vehicle entry granted** ```python @pytest.mark.parametrize("num_vehicles, num_spots", [(2, 1)], indirect=True) class TestTwoVehicleOneSpotAvailable: """Test Case: to verify 2 vehicles entry concurrently""" def test_vehicle_entry_denied(self, parking_lot, vehicles, park_vehicles): """One vehicle should be issued ticket, other should be denied entry""" park_vehicles(parking_lot, vehicles) assert (vehicles[0].ticket is None and vehicles[1].ticket is not None) or ( vehicles[1].ticket is None and vehicles[0].ticket is not None ) def test_num_free_spots_after_vehicle_entrance( self, num_vehicles, num_spots, parking_lot ): """Unit Test to verify number of free spots is updated after vehicle's entry.""" spot_type = ParkingSpotType.COMPACT assert parking_lot._num_free_spots[spot_type] == num_spots - (num_vehicles - 1) ``` - **Unit Test 4: 2 vehicles concurrent entries at multiple entrances** ```python @pytest.mark.parametrize("num_vehicles, num_spots", [(2, 1)], indirect=True) class TestTwoVehicleOneSpotAvailable: """Test Case: to verify 2 vehicles entry concurrently""" def test_vehicle_entry_denied(self, parking_lot, vehicles, park_vehicles): """One vehicle should be issued ticket, other should be denied entry""" park_vehicles(parking_lot, vehicles) assert (vehicles[0].ticket is None and vehicles[1].ticket is not None) or ( vehicles[1].ticket is None and vehicles[0].ticket is not None ) def test_num_free_spots_after_vehicle_entrance( self, num_vehicles, num_spots, parking_lot ): """Unit Test to verify number of free spots is updated after vehicle's entry.""" spot_type = ParkingSpotType.COMPACT assert parking_lot._num_free_spots[spot_type] == num_spots - (num_vehicles - 1) ``` - **Unit Test 5: Spot nearest to entrance allotted** ```python @pytest.mark.parametrize("num_vehicles, num_spots", [(2, 2)], indirect=True) class TestNearestSpotAssigned: """Test Case: to verify if nearest spot to entrance is assigned""" def test_nearest_spots_assigned( self, num_spots, parking_lot, vehicles, park_vehicles ): """Two vehicles should be issued spots at two ends of spots, closest to entry""" park_vehicles(parking_lot, vehicles) assert ( vehicles[0].ticket.spot_id is 0 and vehicles[1].ticket.spot_id is num_spots - 1 ) or ( vehicles[1].ticket.spot_id is 0 and vehicles[0].ticket.spot_id is num_spots - 1 ) def test_num_free_spots_after_vehicle_entrance( self, num_vehicles, num_spots, parking_lot ): """Unit Test to verify number of free spots is updated after vehicle's entry.""" spot_type = ParkingSpotType.COMPACT assert parking_lot._num_free_spots[spot_type] == num_spots - num_vehicles ``` ### Resources - [Code Repository](https://github.com/deepak-karkala/system-design/tree/main/parking_lot) - [API reference](https://deepak-karkala.github.io/system-design/reference/parking_lot/src/main/)