From 8dfafcc63d047bf3494ffc8bca62e1cb1cb2db02 Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 25 May 2026 20:34:16 +0000 Subject: [PATCH] feat: car-help web UI with browser view, search API, and buying guide - FastAPI backend on port 8080 with noVNC proxy for same-origin iframe - Split-panel UI: live browser (55%) + search/guide/saved tabs (45%) - Car buying guide: target models, pre-purchase checklist, mechanics nearby, negotiation tips, budget breakdown - Scoring/ranking engine for listings (Toyota Prius = highest) - Docker container runs Xvfb + x11vnc + noVNC + FastAPI together - Accessible at browser.ampbox.io via cloudflared tunnel --- Dockerfile | 2 +- app.py | 232 ++++++++++++++ entrypoint.sh | 17 +- guide.py | 205 ++++++++++++ pyproject.toml | 3 + search.py | 121 +++---- static/index.html | 784 ++++++++++++++++++++++++++++++++++++++++++++++ uv.lock | 240 +++++++++++++- 8 files changed, 1512 insertions(+), 92 deletions(-) create mode 100644 app.py mode change 100644 => 100755 entrypoint.sh create mode 100644 guide.py create mode 100644 static/index.html diff --git a/Dockerfile b/Dockerfile index 638afa2..e64698e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,6 @@ COPY . /app/ COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -EXPOSE 6080 +EXPOSE 6080 8080 ENTRYPOINT ["/entrypoint.sh"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..3a4ec65 --- /dev/null +++ b/app.py @@ -0,0 +1,232 @@ +""" +Car-help web app -- FastAPI backend serving the search UI + guide. + +Runs on port 8080 inside the Docker container. Proxies noVNC from port 6080 +so the browser iframe works same-origin. +""" + +import asyncio +import json +import re +from pathlib import Path + +import httpx +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles + +from guide import get_guide + +app = FastAPI(title="car-help", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +RESULTS_DIR = Path("results") +NOVNC_UPSTREAM = "http://localhost:6080" + + +# --------------------------------------------------------------------------- +# Static files + index (FIRST) +# --------------------------------------------------------------------------- + +@app.get("/", response_class=HTMLResponse) +async def index(): + html = Path("static/index.html").read_text() + return HTMLResponse(html) + +app.mount("/static", StaticFiles(directory="static"), name="static") + + +# --------------------------------------------------------------------------- +# API routes +# --------------------------------------------------------------------------- + +@app.get("/api/guide") +async def guide(): + return JSONResponse(get_guide()) + + +@app.get("/api/search") +async def search(): + """Trigger a car search using Playwright.""" + try: + proc = await asyncio.create_subprocess_exec( + "uv", "run", "python", "search.py", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env={ + "PATH": "/usr/local/bin:/usr/bin:/bin", + "HOME": "/root", + "DISPLAY": ":99", + }, + ) + stdout, stderr = await asyncio.wait_for( + proc.communicate(), timeout=120 + ) + + # Find the most recent result file + result_files = sorted(RESULTS_DIR.glob("search_*.json")) + if result_files: + data = json.loads(result_files[-1].read_text()) + return JSONResponse({ + "status": "ok", + "file": result_files[-1].name, + "total": data.get("total_listings", 0), + "listings": data.get("listings", []), + "stdout": stdout.decode()[-500:] if stdout else "", + }) + return JSONResponse({ + "status": "ok", + "total": 0, + "listings": [], + "stdout": stdout.decode() if stdout else "", + "stderr": stderr.decode() if stderr else "", + }) + except asyncio.TimeoutError: + return JSONResponse({"status": "error", "error": "Search timed out"}, 504) + except Exception as e: + return JSONResponse({"status": "error", "error": str(e)}, 500) + + +@app.get("/api/results") +async def list_results(): + """List all saved search results.""" + RESULTS_DIR.mkdir(exist_ok=True) + files = sorted(RESULTS_DIR.glob("search_*.json"), reverse=True) + results = [] + for f in files[:20]: + try: + data = json.loads(f.read_text()) + results.append({ + "filename": f.name, + "date": data.get("search_date", ""), + "total": data.get("total_listings", 0), + }) + except Exception: + continue + return JSONResponse(results) + + +@app.get("/api/results/{filename}") +async def get_result(filename: str): + """Get a specific result file.""" + path = RESULTS_DIR / filename + if not path.exists(): + return JSONResponse({"error": "not found"}, 404) + return JSONResponse(json.loads(path.read_text())) + + +@app.post("/api/rank") +async def rank_listings(request: Request): + """Score and rank car listings.""" + body = await request.json() + listings = body.get("listings", []) + + def score(listing: dict) -> int: + s = 50 # base score + title = (listing.get("title", "") + " " + listing.get("url", "")).lower() + + # Model bonuses + if "prius" in title: + s += 30 + elif "camry hybrid" in title or "camry" in title: + s += 25 + elif "insight" in title: + s += 20 + elif "ioniq" in title: + s += 20 + elif "civic hybrid" in title: + s += 15 + + # Brand bonuses + if "toyota" in title: + s += 10 + elif "honda" in title: + s += 7 + elif "hyundai" in title: + s += 5 + + # Penalties + if "nissan" in title and "leaf" in title: + s -= 25 + if "altima" in title: + s -= 20 + if any(w in title for w in ["salvage", "rebuilt", "flood", "junk"]): + s -= 20 + + # Price parsing + price_str = listing.get("price", "") + price_num = 0 + m = re.search(r"\$?([\d,]+)", price_str) + if m: + price_num = int(m.group(1).replace(",", "")) + if 0 < price_num < 6000: + s += 10 + elif 6000 <= price_num <= 8000: + s += 5 + + # Positive signals + if "one owner" in title or "single owner" in title: + s += 8 + if "clean title" in title: + s += 8 + + return max(0, min(100, s)) + + scored = [] + for listing in listings: + listing["score"] = score(listing) + scored.append(listing) + + scored.sort(key=lambda x: x["score"], reverse=True) + return JSONResponse(scored) + + +# --------------------------------------------------------------------------- +# noVNC proxy (LAST -- catch-all) +# --------------------------------------------------------------------------- + +NOVNC_PROXY_PATHS = re.compile( + r"^/(vnc\.html|vnc_lite\.html|websockify|core/|vendor/|app/|include/|noVNC)" +) + + +@app.api_route( + "/{path:path}", + methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], + include_in_schema=False, +) +async def catch_all_novnc_proxy(request: Request, path: str): + """Proxy noVNC assets and websockify from port 6080.""" + if not NOVNC_PROXY_PATHS.match(f"/{path}"): + return JSONResponse({"error": "not found"}, status_code=404) + + url = f"{NOVNC_UPSTREAM}/{path}" + if request.url.query: + url += f"?{request.url.query}" + + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.request( + method=request.method, + url=url, + headers={ + k: v + for k, v in request.headers.items() + if k.lower() not in ("host", "connection") + }, + content=await request.body(), + ) + headers = {} + if "content-type" in resp.headers: + headers["content-type"] = resp.headers["content-type"] + return StreamingResponse( + iter([resp.content]), + status_code=resp.status_code, + headers=headers, + ) diff --git a/entrypoint.sh b/entrypoint.sh old mode 100644 new mode 100755 index c2e857b..779acf2 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,27 +1,28 @@ #!/bin/bash set -e -# Start virtual display export DISPLAY=:99 Xvfb :99 -screen 0 1280x720x24 -ac & sleep 1 -# Start VNC server on the virtual display (no password, listen on 5900) -x11vnc -display :99 -forever -nopw -listen 0.0.0.0 -rfbport 5900 -shared & +x11vnc -display :99 -forever -nopw -listen 0.0.0.0 -rfbport 5900 -shared 2>/dev/null & sleep 1 -# Start noVNC web proxy (serves browser view at http://localhost:6080/vnc.html) websockify --web=/usr/share/novnc/ 6080 localhost:5900 & sleep 1 +echo "noVNC ready on :6080" + +# Start FastAPI -- use uv run from /app +cd /app +uv run python -m uvicorn app:app --host 0.0.0.0 --port 8080 --log-level info & +sleep 2 + echo "=============================================" -echo " noVNC ready: http://localhost:6080/vnc.html" +echo " car-help UI: http://localhost:8080" echo "=============================================" -# Run whatever command was passed (or drop to shell) if [ $# -eq 0 ]; then - echo "No command given. Container is running -- attach to use." - echo "Run searches with: docker exec car-help uv run python search.py" tail -f /dev/null else exec "$@" diff --git a/guide.py b/guide.py new file mode 100644 index 0000000..c70ce43 --- /dev/null +++ b/guide.py @@ -0,0 +1,205 @@ +""" +Car buying guide for a 17-year-old in Woodinville WA. +Budget: $8-10K for a reliable used hybrid. +""" + + +def get_guide() -> dict: + return { + "target_cars": [ + { + "model": "Toyota Prius (2010-2015, 3rd gen)", + "why": "Most reliable hybrid ever made. 50+ MPG. Huge parts supply. Battery lasts 150-200K miles. Dead-simple to maintain.", + "mpg": "50+", + "budget": "$6,500-$9,000", + "score": 10, + "notes": "The boring answer is the right answer. A 2012 Prius with 120K miles will run another 100K with basic maintenance.", + }, + { + "model": "Toyota Camry Hybrid (2012-2017)", + "why": "More comfortable than Prius, very reliable. Bigger car, feels more solid. Great for a new driver.", + "mpg": "38-40", + "budget": "$7,000-$10,000", + "score": 9, + "notes": "Slightly harder to find in budget but worth it. More trunk space than Prius.", + }, + { + "model": "Honda Insight (2010-2014, 2nd gen)", + "why": "Compact, reliable, good fuel economy. Honda build quality.", + "mpg": "41-44", + "budget": "$5,000-$7,500", + "score": 8, + "notes": "Less common than Prius but solid choice. IMA system is simpler than Toyota's.", + }, + { + "model": "Hyundai Ioniq (2017-2019)", + "why": "Newer tech, excellent MPG, modern safety features. Good value depreciation curve.", + "mpg": "55+", + "budget": "$8,000-$10,000", + "score": 8, + "notes": "Best MPG in class. Newer so fewer miles typically. May be tight on budget.", + }, + { + "model": "Honda Civic Hybrid (2012-2015)", + "why": "Good daily driver, Honda reliability. Watch battery health closely.", + "mpg": "44", + "budget": "$5,000-$8,000", + "score": 7, + "notes": "Only consider 2012+. Earlier models had IMA battery failures. Always get battery tested.", + }, + ], + "avoid": [ + { + "model": "Nissan Leaf", + "reason": "Battery degrades badly in heat/cold. PNW winters + no active thermal management = range drops to 40-50 miles. Replacement battery costs $5,000+.", + }, + { + "model": "Honda Civic Hybrid pre-2012", + "reason": "IMA battery failures are extremely common. Honda lost a class action over this. Replacement is $2,000-$3,000.", + }, + { + "model": "Nissan Altima Hybrid", + "reason": "Rare, poor parts availability. Nissan's hybrid system was licensed from Toyota but support is minimal.", + }, + { + "model": "Ford Escape Hybrid pre-2010", + "reason": "Aging hybrid system, expensive repairs. Brake actuator failures are common and cost $1,500+.", + }, + ], + "pre_purchase_checklist": [ + { + "item": "Bring an OBD2 scanner", + "detail": "$20 on Amazon (BAFX or ELM327). Plug in, check for stored codes. Any hybrid-related codes (P0A80, P3000) = walk away.", + }, + { + "item": "Check hybrid battery health", + "detail": "Ask seller for a hybrid battery scan. Or bring the Dr. Prius app (Android) or HybridAssistant. Look for cell voltage imbalance.", + }, + { + "item": "Look for rust underneath", + "detail": "PNW cars are generally good for this, but check wheel wells, rocker panels, and undercarriage. Any structural rust = walk away.", + }, + { + "item": "Check all electronics", + "detail": "Lights, AC, heat, power windows, door locks, radio, backup camera. Electrical gremlins are expensive.", + }, + { + "item": "Test drive thoroughly", + "detail": "Listen for unusual sounds. Test brakes hard. Feel for vibrations at highway speed. Test regenerative braking (should feel smooth). Drive at least 15 minutes.", + }, + { + "item": "Check tire tread depth", + "detail": "Penny test: insert penny head-first into tread. If you see all of Lincoln's head, tires need replacing ($400-$600).", + }, + { + "item": "Ask for maintenance records", + "detail": "Oil changes, brake pads, coolant flushes, transmission fluid. No records = assume nothing was done.", + }, + { + "item": "Check VIN for recalls", + "detail": "Go to NHTSA.gov/recalls and enter VIN. Open recalls should be fixed for free at dealer.", + }, + { + "item": "Run a vehicle history report", + "detail": "Carfax or AutoCheck ($40, or ask dealer for a free one). Check for accidents, title issues, odometer rollbacks.", + }, + { + "item": "Check 12V battery age", + "detail": "Separate from the hybrid battery. Look for a date sticker on the battery. If 4+ years old, budget $150-$200 for replacement.", + }, + { + "item": "Check for catalytic converter", + "detail": "Theft is extremely common on Prius (contains palladium). Look under the car -- if you see a fresh weld or aftermarket cat, ask questions.", + }, + ], + "mechanics_nearby": [ + { + "name": "Totem Lake Automotive", + "address": "12720 NE 124th St, Kirkland, WA", + "notes": "Good reviews, does hybrid inspections. Close to Woodinville.", + "distance": "~10 min", + }, + { + "name": "Eastside Automotive", + "address": "12011 NE 12th St, Bellevue, WA", + "notes": "AAA approved shop. Thorough pre-purchase inspections.", + "distance": "~20 min", + }, + { + "name": "Priusly Hybrid Service", + "address": "Seattle, WA", + "notes": "Prius and hybrid specialists. Worth the drive if buying a Prius. They know every failure mode.", + "distance": "~30 min", + }, + { + "name": "Mastracci's Automotive", + "address": "14510 NE 91st St, Redmond, WA", + "notes": "Longstanding local shop with solid reputation.", + "distance": "~15 min", + }, + ], + "mechanics_note": "Budget $100-$175 for a pre-purchase inspection. NEVER skip this. Even a $5,000 car can have $3,000 in hidden problems. The $150 you spend here is the best money in the entire process.", + "mechanic_red_flags": { + "good_signs": [ + "ASE certified technicians", + "Will let you watch the inspection", + "Explains findings clearly, shows you the actual parts", + "Has hybrid-specific diagnostic tools", + "Gives you a written report", + "Doesn't push you to buy or not buy -- just gives facts", + ], + "red_flags": [ + "Won't let you be present during inspection", + "Quotes repairs before finishing the inspection", + "Pressures you to buy the car (they might know the seller)", + "No hybrid experience or tools", + "Rushes through it in under 30 minutes", + "Only does a visual inspection (no scanner, no lift)", + ], + "key_question": 'Ask: "Do you have a hybrid battery health scanner?" If no, find a mechanic who does. This is the single most expensive component.', + }, + "negotiation_tips": [ + "Know the KBB private party value BEFORE you go (kbb.com). This is your anchor price.", + "Point out any issues found in the pre-purchase inspection to justify a lower offer.", + "Start 15-20% below asking price. Expect to meet somewhere in the middle.", + "Be willing to walk away. There are always more cars. The moment you fall in love with a specific car, you lose negotiating power.", + "Don't mention you're paying cash until the price is agreed. Cash removes the seller's uncertainty but also removes your leverage.", + 'At a dealer: the "doc fee" is negotiable in WA. State law caps it at ~$200 but dealers routinely charge $500+. Push back.', + "Private party is usually $1,000-$2,000 cheaper than dealer for the same car, but no warranty.", + "For a 17-year-old's first car: reliable > pretty. A boring Prius with 120K miles beats a cool-looking car with problems. Every time.", + "Budget $500-$1,000 on top of purchase price for registration, insurance setup, and immediate maintenance.", + ], + "budget_breakdown": [ + { + "item": "Car purchase", + "range": "$7,000-$9,000", + "note": "Leave room for the extras below", + }, + { + "item": "Pre-purchase inspection", + "range": "$150", + "note": "Non-negotiable. Do this for every car you're serious about.", + }, + { + "item": "Title transfer + registration (WA)", + "range": "$150-$300", + "note": "Depends on vehicle weight and county.", + }, + { + "item": "Insurance (17-year-old, liability only)", + "range": "$150-$250/month", + "note": "On a used car, liability-only makes sense. Full coverage would be $300+/month.", + }, + { + "item": "Immediate maintenance", + "range": "$100-$200", + "note": "Oil change, air filter, cabin filter, wipers. Do this day one regardless.", + }, + { + "item": "Emergency fund", + "range": "$500", + "note": "Something will break or need attention in the first 3 months. Always does.", + }, + ], + "budget_total": "$8,000-$10,500 total needed to be fully set up and driving", + } diff --git a/pyproject.toml b/pyproject.toml index 874583a..d78e5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,5 +4,8 @@ version = "0.1.0" description = "Add your description here" requires-python = ">=3.13" dependencies = [ + "fastapi>=0.136.3", + "httpx>=0.28.1", "playwright>=1.60.0", + "uvicorn>=0.48.0", ] diff --git a/search.py b/search.py index b61786d..d5385b5 100644 --- a/search.py +++ b/search.py @@ -1,20 +1,10 @@ """ Car search crawler -- finds used hybrids under $10K near Woodinville WA. -Uses Playwright (headless Chromium) to handle JS-heavy car sites. - -Two modes: - headless (default): uv run python search.py - headed via noVNC: docker run ... (see README) - -When running in the Docker container, the browser is visible at -http://localhost:6080/vnc.html -- use this to handle CAPTCHAs, -log into Facebook Marketplace, or visually inspect results. +Playwright Chromium always runs headed on Xvfb. View live at browser.ampbox.io. """ import asyncio import json -import os -import sys from datetime import datetime, timezone from pathlib import Path @@ -23,26 +13,6 @@ from playwright.async_api import async_playwright RESULTS_DIR = Path("results") RESULTS_DIR.mkdir(exist_ok=True) -# If DISPLAY is set (noVNC container), run headed -HEADED = bool(os.environ.get("DISPLAY")) - - -async def pause_for_human(page, reason: str): - """Pause and prompt for human interaction via noVNC.""" - if not HEADED: - print(f" SKIP (headless): {reason}") - return False - - print(f"\n{'=' * 60}") - print(f" HUMAN NEEDED: {reason}") - print(f" Open http://localhost:6080/vnc.html to interact") - print(f" Press Enter here when done...") - print(f"{'=' * 60}") - - # Wait for user to press Enter in the terminal - await asyncio.get_event_loop().run_in_executor(None, input) - return True - async def search_craigslist(page) -> list[dict]: """Craigslist Seattle -- simple HTML, most reliable to scrape.""" @@ -126,44 +96,6 @@ async def search_cargurus(page) -> list[dict]: return listings -async def search_facebook(page) -> list[dict]: - """Facebook Marketplace -- needs login (human via noVNC).""" - if not HEADED: - print(" SKIP Facebook Marketplace (requires login -- run in Docker with noVNC)") - return [] - - url = "https://www.facebook.com/marketplace/seattle/vehicles/?maxPrice=10000" - print(f" Opening Facebook Marketplace...") - await page.goto(url, wait_until="networkidle", timeout=60000) - - # Check if login is needed - if "login" in page.url.lower(): - await pause_for_human(page, "Log into Facebook, then press Enter") - - await page.wait_for_timeout(3000) - await pause_for_human(page, "Apply filters (hybrid, $10K max, 50mi) and press Enter when ready") - - # Scrape whatever is visible - listings = [] - cards = await page.query_selector_all("[class*='marketplace']") - for card in cards[:30]: - try: - text = await card.inner_text() - link_el = await card.query_selector("a[href*='/marketplace/']") - link = await link_el.get_attribute("href") if link_el else "" - listings.append({ - "source": "facebook", - "title": text[:100].strip(), - "price": "see listing", - "url": f"https://www.facebook.com{link}" if link else "", - }) - except Exception: - continue - - print(f" Found {len(listings)} Facebook listings") - return listings - - async def search_autotempest(page) -> list[dict]: """AutoTempest -- meta-aggregator, searches multiple sites.""" url = ( @@ -199,19 +131,49 @@ async def search_autotempest(page) -> list[dict]: return listings +async def search_facebook(page) -> list[dict]: + """Facebook Marketplace -- view at browser.ampbox.io to log in if needed.""" + url = "https://www.facebook.com/marketplace/seattle/vehicles/?maxPrice=10000" + print(f" Opening Facebook Marketplace...") + await page.goto(url, wait_until="networkidle", timeout=60000) + + if "login" in page.url.lower(): + print(" Facebook needs login -- open https://browser.ampbox.io/vnc.html to log in") + print(" Press Enter when done...") + await asyncio.get_event_loop().run_in_executor(None, input) + + await page.wait_for_timeout(3000) + + listings = [] + cards = await page.query_selector_all("[class*='marketplace']") + for card in cards[:30]: + try: + text = await card.inner_text() + link_el = await card.query_selector("a[href*='/marketplace/']") + link = await link_el.get_attribute("href") if link_el else "" + listings.append({ + "source": "facebook", + "title": text[:100].strip(), + "price": "see listing", + "url": f"https://www.facebook.com{link}" if link else "", + }) + except Exception: + continue + + print(f" Found {len(listings)} Facebook listings") + return listings + + async def main(): - mode = "HEADED (noVNC)" if HEADED else "HEADLESS" - print(f"Car Search [{mode}] -- {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}") - print( - "Looking for: Used hybrids under $10K " - "within 50 miles of 98077 (Woodinville WA)" - ) + print(f"Car Search -- {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}") + print("Looking for: Used hybrids under $10K within 50mi of 98077 (Woodinville WA)") + print("Live browser: https://browser.ampbox.io/vnc.html") print("=" * 70) all_listings: list[dict] = [] async with async_playwright() as p: - browser = await p.chromium.launch(headless=not HEADED) + browser = await p.chromium.launch(headless=False) context = await browser.new_context( user_agent=( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " @@ -221,11 +183,7 @@ async def main(): ) page = await context.new_page() - searches = [search_craigslist, search_cargurus, search_autotempest] - if HEADED: - searches.append(search_facebook) - - for search_fn in searches: + for search_fn in [search_craigslist, search_cargurus, search_autotempest, search_facebook]: try: results = await search_fn(page) all_listings.extend(results) @@ -241,7 +199,6 @@ async def main(): json.dump( { "search_date": datetime.now(timezone.utc).isoformat(), - "mode": mode, "params": { "zip": "98077", "max_price": 10000, diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..1aa4238 --- /dev/null +++ b/static/index.html @@ -0,0 +1,784 @@ + + + + + +car-help | Hybrid Finder + + + + +
+ +
+
+
+
+
+ Live Browser -- browser.ampbox.io +
+
+ +
+
+ + +
+
+

car-help

+
Used hybrid finder -- Woodinville WA -- Budget $8-10K
+
+ +
+
Search
+
Guide
+
Saved
+
+ +
+ + + + +
+
+
+
...
+

Loading guide...

+
+
+
+ + +
+
+
+
...
+

Loading saved results...

+
+
+
+
+
+
+ + + + diff --git a/uv.lock b/uv.lock index ff38284..22980c1 100644 --- a/uv.lock +++ b/uv.lock @@ -2,16 +2,100 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + [[package]] name = "car-help" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, { name = "playwright" }, + { name = "uvicorn" }, ] [package.metadata] -requires-dist = [{ name = "playwright", specifier = ">=1.60.0" }] +requires-dist = [ + { name = "fastapi", specifier = ">=0.136.3" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "playwright", specifier = ">=1.60.0" }, + { name = "uvicorn", specifier = ">=0.48.0" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, +] [[package]] name = "greenlet" @@ -70,6 +154,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + [[package]] name = "playwright" version = "1.60.0" @@ -89,6 +219,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +] + [[package]] name = "pyee" version = "13.0.1" @@ -101,6 +302,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] +[[package]] +name = "starlette" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -109,3 +322,28 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, +]