# OEM Vehicle Stock Crawler A production-ready, modular platform for crawling car manufacturer (OEM) inventory, tracking vehicle stock, specifications, optional equipment, and price changes over time. --- ## πŸ“ Project Architecture ``` oem_crawler/ β”œβ”€β”€ Dockerfile # Lean production container using uv β”œβ”€β”€ docker-compose.yml # PostgreSQL 16 + crawler container orchestrator β”œβ”€β”€ .env.example # Environment configuration template β”œβ”€β”€ pyproject.toml # Dependencies and CLI entrypoints (`oem-crawler`, `oem-inspect`) β”œβ”€β”€ schema.sql # PostgreSQL database DDL β”‚ β”œβ”€β”€ data/ # Development SQLite databases (gitignored) β”‚ └── crawler_stock.db # Default local SQLite database file β”‚ β”œβ”€β”€ src/oem_crawler/ β”‚ β”œβ”€β”€ config.py # Environment configuration (pydantic-settings) β”‚ β”œβ”€β”€ cli.py # Main CLI application (`oem-crawler`) β”‚ β”‚ β”‚ β”œβ”€β”€ tools/ # Developer & stock inspection tools β”‚ β”‚ β”œβ”€β”€ README.md # Tools documentation and sqlite3 recipes β”‚ β”‚ └── view_stock.py # Terminal inventory inspector (`oem-inspect`) β”‚ β”‚ β”‚ β”œβ”€β”€ db/ # Database Layer β”‚ β”‚ β”œβ”€β”€ models.py # SQLAlchemy 2.0 ORM mappings β”‚ β”‚ β”œβ”€β”€ schemas.py # Pydantic v2 validation models & SHA-256 hashing β”‚ β”‚ β”œβ”€β”€ session.py # Connection and session factory β”‚ β”‚ └── pipeline.py # Ingestion pipeline, change-detection & delisting β”‚ β”‚ β”‚ └── crawlers/ # Modular OEM Crawlers β”‚ β”œβ”€β”€ base.py # BaseCrawler interface & CrawlerRegistry β”‚ β”œβ”€β”€ audi/ # Audi Implementation β”‚ β”‚ β”œβ”€β”€ crawler.py # Stock Car Service (SCS) REST client β”‚ β”‚ └── parser.py # PR option codes, EV specs & GPS coordinates β”‚ β”œβ”€β”€ bmw/ # BMW Implementation β”‚ β”‚ β”œβ”€β”€ crawler.py # REST API client & pagination β”‚ β”‚ └── parser.py # OEM response normalization β”‚ β”œβ”€β”€ mercedes/ # Mercedes-Benz Implementation β”‚ β”‚ β”œβ”€β”€ crawler.py # OneSearch GraphQL + PLS + VPC client β”‚ β”‚ └── parser.py # OneWeb normalization logic β”‚ β”‚ └── query.py # Tested GraphQL query definitions β”‚ └── porsche/ # Porsche Implementation β”‚ β”œβ”€β”€ crawler.py # Finder Next.js RSC client & pagination β”‚ └── parser.py # Option C, equipment & metadata normalization β”‚ └── tests/ # Automated Test Suite β”œβ”€β”€ test_schema.py # Schema & hashing unit tests β”œβ”€β”€ test_audi_parser.py # Audi parser & registry unit tests β”œβ”€β”€ test_bmw_parser.py # BMW parser & registry unit tests β”œβ”€β”€ test_mercedes_parser.py # Mercedes parser unit tests β”œβ”€β”€ test_porsche_parser.py # Porsche parser unit tests └── test_pipeline.py # Ingestion pipeline unit tests ``` --- ## πŸ”Œ Adding a New Car Manufacturer Crawler Adding a new OEM crawler (e.g. Mercedes, Audi, Tesla, Porsche) takes 2 simple steps: 1. Create a new folder under `src/oem_crawler/crawlers//` (e.g. `mercedes/`): 2. Implement your crawler inheriting from `BaseCrawler` and register it: ```python from oem_crawler.crawlers.base import BaseCrawler, CrawlerRegistry from oem_crawler.db.schemas import VehicleIngestSchema @CrawlerRegistry.register("mercedes") class MercedesStockCrawler(BaseCrawler): @property def brand_name(self) -> str: return "Mercedes-Benz" def crawl(self, max_vehicles=None, page_size=20, search_filter=None): # 1. Fetch from OEM API / site # 2. Parse into VehicleIngestSchema # 3. Yield parsed vehicle ... ``` The CLI (`oem-crawler --brand mercedes`) and pipeline will automatically discover and support it. --- ## 🐳 Running with Docker ### 1. Start PostgreSQL Database & Run a Crawl ```bash docker compose up --build ``` ### 2. Run Ad-hoc Crawl inside Docker ```bash # Fetch 100 vehicles for BMW docker compose run --rm crawler --brand bmw --limit 100 ``` ### 3. Query PostgreSQL Database inside Docker You can query the database directly in your terminal using `docker compose exec`: ```bash # View latest vehicles in stock docker compose exec postgres psql -U crawler_user -d oem_stock_db -c \ "SELECT brand, series, model_name, price_gross, exterior_color, location_city FROM vehicles LIMIT 10;" # View stored factory equipment & options for a vehicle docker compose exec postgres psql -U crawler_user -d oem_stock_db -c \ "SELECT option_code, name, category, is_standard, price_gross FROM vehicle_options LIMIT 15;" # View crawl run history and telemetry docker compose exec postgres psql -U crawler_user -d oem_stock_db -c \ "SELECT id, brand, status, items_discovered, items_inserted, duration_ms, started_at FROM crawl_runs;" # View price and status history snapshots docker compose exec postgres psql -U crawler_user -d oem_stock_db -c \ "SELECT vehicle_id, price_gross, monthly_rate, status, recorded_at FROM vehicle_price_history LIMIT 10;" ``` ### 4. Connect with GUI Database Clients (DBeaver, TablePlus, pgAdmin) - **Host**: `localhost` - **Port**: `5432` - **Database**: `oem_stock_db` - **User**: `crawler_user` - **Password**: `crawler_password` --- ## πŸ’» Local Development (without Docker) ### 1. Run Unit Tests ```bash uv run pytest ``` ### 2. Run Local Crawl & Persistent Ingestion > **Note**: The default local database is automatically stored in `data/crawler_stock.db` (gitignored). You do not need to specify `--db-url` unless targeting a custom location or PostgreSQL. ```bash # Crawl New BMW Vehicles (Neuwagensuche) uv run oem-crawler --brand bmw --stock-type new --limit 50 # Crawl New Mercedes-Benz Vehicles (Neuwagensuche via OneSearch GraphQL + PLS/VPC) uv run oem-crawler --brand mercedes --stock-type new --limit 50 # Crawl Used Mercedes-Benz Vehicles (Gebrauchtwagen via gebrauchtwagen.mercedes-benz.de) uv run oem-crawler --brand mercedes --stock-type used --limit 50 # Crawl Both New & Used Vehicles uv run oem-crawler --brand mercedes --stock-type all --limit 100 # Crawl Used BMW Vehicles (Gebrauchtwagen with mileage & first registration) uv run oem-crawler --brand bmw --stock-type used --limit 50 # Crawl New Porsche Vehicles (Finder Neuwagen via Next.js RSC) uv run oem-crawler --brand porsche --stock-type new --limit 50 # Crawl Used / Approved Porsche Vehicles (Finder Gebrauchtwagen via Next.js RSC) uv run oem-crawler --brand porsche --stock-type used --limit 50 # Crawl Both New & Used Porsche Vehicles uv run oem-crawler --brand porsche --stock-type all --limit 100 # Or export Porsche stock directly to local JSON uv run oem-crawler --brand porsche --stock-type new --limit 20 --output-json porsche_new_stock.json # Crawl New Audi Vehicles (NeuwagenbΓΆrse via Stock Car Service scs.audi.de) uv run oem-crawler --brand audi --stock-type new --limit 50 # Crawl Used Audi Vehicles (GebrauchtwagenbΓΆrse with mileage, dealer GPS & PR options) uv run oem-crawler --brand audi --stock-type used --limit 50 # Crawl Both New & Used Audi Vehicles uv run oem-crawler --brand audi --stock-type all --limit 100 # --- Full Scans & Multi-Brand Ingestion --- # πŸš€ Full Scan across ALL Brands (Audi, BMW, Mercedes-Benz, Porsche) uv run oem-crawler --all-brands --full-scan --delay 0.3 # Full Scan across all brands for New Cars only uv run oem-crawler --all-brands --stock-type new --full-scan --delay 0.3 # Scan a custom selection of brands in one command uv run oem-crawler --brand bmw,audi,porsche --full-scan --delay 0.3 ``` ### 3. Inspect Stored Inventory in Terminal (`oem-inspect`) The project includes an inspection and auditing utility located in `src/oem_crawler/tools/`: ```bash # View inventory summary and brand breakdown table uv run oem-inspect --summary # View first 10 vehicles across all brands uv run oem-inspect --limit 10 # Filter by brand uv run oem-inspect --brand audi --limit 5 uv run oem-inspect --brand porsche --limit 10 # Or run via Python module directly uv run python -m oem_crawler.tools.view_stock --summary ``` For more options and direct SQLite query recipes, see the [Tools Documentation](src/oem_crawler/tools/README.md).