CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-travel

Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), traffic-aware drive planning for in-person meetings (auto drive blocks + leave-by traffic rechecks), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay plugin.

69

Quality

86%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

tripit_flights.pyskills/drive-engine/

"""TripIt Flight-segment → Flight normalization for the byAir ∪ TripIt union (R2).

The live sweep's flights come from byAir, but #156 R2 wants the UNION: a flight
tracked by EITHER source produces legs, so a TripIt segment byAir never ingested
(manual booking, unparsed email) is not silently dropped. `merge_flights` collapses
a byAir flight and its TripIt twin by (route, scheduled instant), so feeding both is
safe — duplicates fuse, singletons survive.

TripIt's travel-schedule Flight segments carry airports only in free text — the iCal
`[Flight] ATL to SJO` shape (regular, code-based), plus a designator in the summary.
That is bounded enough to parse (two IATA codes around `to`); a segment that doesn't
match is skipped rather than guessed, so a malformed row never fabricates a flight.
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

_BUNDLE_DIR = Path(__file__).resolve().parent
if str(_BUNDLE_DIR) not in sys.path:
    sys.path.insert(0, str(_BUNDLE_DIR))

from flight_identity import Flight  # noqa: E402
from normalize import flight_from_tripit_segment  # noqa: E402

# TripIt iCal renders a flight route as "<DEP> to <ARR>" with 3-letter IATA codes,
# e.g. "[Flight] ATL to SJO" (description) or "DL 4908 ATL to SJO" (summary).
_ROUTE_RE = re.compile(r"\b([A-Z]{3})\s+to\s+([A-Z]{3})\b")
# A leading IATA flight designator in the summary, e.g. "DL 4908", "FR7382".
_CODE_RE = re.compile(r"\b([A-Z]{2}|[A-Z]\d|\d[A-Z])\s?(\d{1,4})\b")
# TripIt's per-item DESCRIPTION carries the trip URL "trip/show/id/<n>"; the id
# groups a trip's legs so the chain planner sees connections (#156 §D). Stored
# NEGATED so a TripIt trip id can never collide with a byAir (positive) trip_id.
_TRIP_ID_RE = re.compile(r"trip/show/id/(\d+)")


def _route(segment: dict) -> tuple[str, str] | None:
    for field in ("description", "summary"):
        text = segment.get(field)
        if isinstance(text, str):
            m = _ROUTE_RE.search(text)
            if m:
                return m.group(1), m.group(2)
    return None


def _code(segment: dict) -> str | None:
    summary = segment.get("summary")
    if isinstance(summary, str):
        m = _CODE_RE.search(summary)
        if m:
            return f"{m.group(1)}{m.group(2)}"
    return None


def _trip_id(segment: dict) -> int | None:
    """The trip's grouping id, negated to avoid colliding with byAir trip_ids."""
    description = segment.get("description")
    if isinstance(description, str):
        m = _TRIP_ID_RE.search(description)
        if m:
            return -int(m.group(1))
    return None


def flights_from_schedule(schedule: list[dict] | None) -> list[Flight]:
    """Normalize the schedule's `Flight` segments into TripIt-source `Flight`s.

    Only segments with a parseable route AND a parseable start become flights; the
    rest are skipped (never guessed). Airports are the IATA codes from the route
    text; times from `start` / `end`.
    """
    flights: list[Flight] = []
    for segment in schedule or []:
        if not isinstance(segment, dict) or segment.get("type") != "Flight":
            continue
        route = _route(segment)
        if route is None:
            continue
        dep_iata, arr_iata = route
        try:
            flights.append(
                flight_from_tripit_segment(
                    segment,
                    dep_iata=dep_iata,
                    arr_iata=arr_iata,
                    code=_code(segment),
                    trip_id=_trip_id(segment),
                )
            )
        except ValueError:
            # Missing uid / unparseable start — skip, don't fabricate a flight.
            continue
    return flights

README.md

tile.json