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 tile.
77
96%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
"""Assemble a flight's airport drive blocks from live byAir + Maps inputs.
Piece 4c of #90 (the integration). This is the I/O-bearing layer that turns a
flight's persisted state into the two `DesiredDriveBlock`s the pure planner
`airport_drive.plan_drive_block` reconciles against the calendar:
flight state ──► airport_drive_reconcile ──► [DesiredDriveBlock, ...] ──► plan_drive_block
For each direction it needs to build, it resolves the airport context
(`byair.get_airport` → flag / `delay.index` / IANA tz / code, via
`airport_drive_inputs.airport_context`), routes the drive leg
(`maps.travel_time`), picks the byAir-truth dep/arr instant from the snapshot,
and hands those to `airport_drive_inputs.departure_block` / `arrival_block`. The
window math, summaries, and tz selection live there; this module is the glue
that feeds them live data.
`run_airport_drive_reconcile` is the orchestration on top: for each active
flight it assembles the desired blocks, fetches the primary calendar once across
the spanning window, runs `plan_drive_block` per block, and executes the create /
shift ops via Composio. Calendar-as-state, no ledger — an existing block is found
by its marker and its no-op `signature` is derived from the block's OWN stored
state (the `anchor` + baseline it carries), not from Google's start/end echo, so
the comparison is round-trip-stable regardless of how the calendar API formats
the offset. A shift past the re-anchor threshold is a recreate-then-delete (create
first so there is never a gap, roll the new one back if the old delete fails so
there is never a duplicate), so every write goes through the timezone-correct
`build_block_args` create path; a
sub-threshold drift (traffic jitter under `_REANCHOR_THRESHOLD`) is left alone so
the block does not thrash the calendar every poll (#90 §7). The fetch window is
anchored on the flight's stable scheduled times so a block created before a delay
is still found (and shifted, not duplicated) however far the flight has moved.
Which blocks get built is gated on the flight's `computed_status`:
* `to_airport` — while the flight has not left (scheduled / check-in / boarding).
* `from_airport` — once it is in the air or down (departed / en_route / landed),
so the drive-home block appears early and re-anchors as the ETA firms up
(#90 §6 (c)).
Routing endpoints mirror the precheck's existing time-to-leave query: the
airport leg endpoint is the airport `name` (falling back to `code`), which
Distance Matrix resolves, and which reads cleanly as the block's calendar
location. The recheck poll later re-routes exactly the stored origin/destination
pair, so both are captured on the block.
Errors degrade per leg, never abort: a byAir or Maps failure for one direction
drops just that block (logged to stderr) and the next cycle retries — one bad
airport lookup never costs the other block or the other flight.
`run_airport_drive_pass` is the wake-cycle entry point the reconcile script
calls: it resolves the inputs from the environment + on-disk state (config
`home_address`, the live origin via `state.resolve_live_origin`, the byAir + Maps
clients, the active flights' states) and runs the reconcile, staying dormant
(idle summary) when routing is unavailable.
No external dependencies (`jbaruch/coding-policy: dependency-management`); imports
are sibling flight-assist modules resolved on `sys.path` at runtime.
"""
from __future__ import annotations
import os
import sys
import urllib.error
from datetime import datetime, timedelta, timezone
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 airport_block import parse_block # noqa: E402
from airport_drive import DesiredDriveBlock, plan_drive_block # noqa: E402
from airport_drive_inputs import ( # noqa: E402
AirportContext,
airport_context,
arrival_block,
departure_block,
)
from byair_client import ByAirClient, ByAirError # noqa: E402
# Reuse the live-verified Composio event-fetch shaping from calendar_reconcile
# (the v3 response nesting in `_items` was verified against the NAS) rather than
# duplicate it here — same skill bundle, no circular import (calendar_reconcile
# does not import this module).
from calendar_reconcile import _created_event_id, _find_events_args, _items # noqa: E402
from composio_client import ComposioError # noqa: E402
from maps_client import MapsClient, MapsError # noqa: E402
from state import ( # noqa: E402
read_active_flights,
read_config,
read_flight_state,
resolve_live_origin,
)
# A re-routed leave-by that drifts less than this from the block already on the
# calendar is left in place — traffic jitter under it must not rewrite the event
# every poll (#90 §7 "shift only past a ~5-min threshold").
_REANCHOR_THRESHOLD = timedelta(minutes=5)
# Padding on the primary-calendar fetch window. It must cover the distance from
# a flight time to its drive block (max clearance 120 min + the drive, or the
# post-arrival delay + the drive), so that — with the window also anchored on the
# flight's STABLE scheduled times — a block created before a delay is still
# fetched no matter how far the flight has since shifted. Anchoring on scheduled
# times (not just the delayed desired window) is what makes this independent of
# the delay magnitude: a pad sized to the delay would be unbounded.
_FETCH_WINDOW_PAD = timedelta(hours=5)
# Where the airport drive blocks live (#90 decision: the primary calendar,
# alongside drive-planner's `Drive:` meeting blocks).
PRIMARY_CALENDAR_ID = "primary"
# `computed_status` values that gate each direction. Before the flight leaves,
# the drive TO the departure airport may still be needed; once it is airborne or
# down, the drive HOME from the arrival airport is what matters. A cancelled /
# diverted flight builds neither here (its teardown is a separate concern).
_TO_AIRPORT_STATUSES = frozenset({"scheduled", "check_in_open", "boarding"})
_FROM_AIRPORT_STATUSES = frozenset({"departed", "en_route", "landed"})
def _parse_instant(raw: object) -> datetime | None:
"""Parse a byAir / scheduled time string into a tz-aware datetime, or None."""
if not isinstance(raw, str) or not raw:
return None
try:
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed
def _effective_instant(state: dict, snapshot_key: str, scheduled_key: str) -> datetime | None:
"""The byAir-truth instant for a leg: snapshot actual when usable, else scheduled.
`last_snapshot.<dep|arr>_time` is byAir's live value (the ETA in flight, the
actual once known); the sync-seeded scheduled time is the fallback. Like
`calendar_reconcile._effective_times` it prefers the snapshot actual — but it
deliberately diverges on a present-but-unparseable snapshot value (e.g. an
empty string from bad byAir data): `_effective_times` would use it as-is,
whereas this falls back to scheduled rather than suppressing the block, and
logs the bad value (silent bad data contradicts the per-leg degradation
posture). Returns None only when scheduled is unusable too — the caller logs
that drop.
"""
snapshot = state.get("last_snapshot") or {}
raw = snapshot.get(snapshot_key)
instant = _parse_instant(raw)
if instant is not None:
return instant
if raw is not None:
print(
f"flight-assist airport-drive: unparseable {snapshot_key}={raw!r}; "
f"falling back to {scheduled_key}",
file=sys.stderr,
)
return _parse_instant(state.get(scheduled_key))
def _safe_airport_context(byair, airport_id: object) -> AirportContext:
"""Fetch + extract an airport context, or an empty context on any failure.
An empty context classifies the route international (the safe, over-buffering
direction) and omits the tz — never raises. A byAir error here for the
SECONDARY airport of a direction is tolerable; the caller separately requires
the PRIMARY airport's code before building a block.
"""
if not isinstance(airport_id, int) or isinstance(airport_id, bool):
return AirportContext()
try:
payload = byair.get_airport(airport_id)
except (ByAirError, urllib.error.URLError) as exc:
print(
f"flight-assist airport-drive: get_airport({airport_id}) failed: {exc}",
file=sys.stderr,
)
return AirportContext()
return airport_context(payload)
def _route_seconds(maps, *, origin: str, destination: str) -> int | None:
"""Routed drive seconds (traffic-aware when available), or None on failure.
Returns `in_traffic_seconds` when the provider modelled traffic, else the
free-flow `duration_seconds` — matching `precheck._maybe_query_travel_time`.
A Maps or transport error drops just this leg.
"""
try:
result = maps.travel_time(origin=origin, destination=destination)
except (MapsError, urllib.error.URLError) as exc:
print(f"flight-assist airport-drive: maps route failed: {exc}", file=sys.stderr)
return None
# `is not None`, not truthiness — a modelled 0-second in-traffic estimate is a
# valid time and must not fall back to free-flow.
if result.in_traffic_seconds is not None:
return result.in_traffic_seconds
return result.duration_seconds
def _clean_endpoint(value: str | None) -> str | None:
"""A stripped, non-empty endpoint string, or None when absent / blank.
A resolved `origin` / `home_address` can arrive as `""` or whitespace (an
empty config value, an origin ladder that produced nothing usable). The real
`MapsClient.travel_time` raises `ValueError` on a blank endpoint — which
`_route_seconds` does not catch — so a blank string must read as absent here
(the leg is skipped) rather than enter routing and abort the assembler.
"""
if not isinstance(value, str):
return None
stripped = value.strip()
return stripped or None
def _airport_endpoint(ctx: AirportContext) -> str | None:
"""The routable / displayable string for an airport leg endpoint."""
return ctx.name or ctx.code
def _build_departure(
state: dict,
*,
dep_ctx: AirportContext,
arr_ctx: AirportContext,
origin: str,
maps,
flight_code: str,
config: dict | None,
) -> DesiredDriveBlock | None:
"""Assemble the drive-TO-departure-airport block, or None if not buildable."""
if not dep_ctx.code:
return None # no airport code → no usable summary; retry next cycle
dep_instant = _effective_instant(state, "dep_time", "scheduled_dep_time")
if dep_instant is None:
print(
f"flight-assist airport-drive: no usable departure time for "
f"{flight_code or state.get('flight_id')}; dropping to_airport block",
file=sys.stderr,
)
return None
destination = _airport_endpoint(dep_ctx)
if not destination:
return None
seconds = _route_seconds(maps, origin=origin, destination=destination)
if seconds is None:
return None
return departure_block(
flight_code=flight_code,
dep_code=dep_ctx.code,
dep_ctx=dep_ctx,
arr_ctx=arr_ctx,
dep_instant=dep_instant,
origin=origin,
destination=destination,
baseline_seconds=seconds,
config=config,
)
def _build_arrival(
state: dict,
*,
dep_ctx: AirportContext,
arr_ctx: AirportContext,
home_address: str,
maps,
config: dict | None,
) -> DesiredDriveBlock | None:
"""Assemble the drive-HOME-from-arrival-airport block, or None if not buildable."""
if not arr_ctx.code:
return None
arr_instant = _effective_instant(state, "arr_time", "scheduled_arr_time")
if arr_instant is None:
print(
f"flight-assist airport-drive: no usable arrival time for "
f"{state.get('code') or state.get('flight_id')}; dropping from_airport block",
file=sys.stderr,
)
return None
origin = _airport_endpoint(arr_ctx)
if not origin:
return None
seconds = _route_seconds(maps, origin=origin, destination=home_address)
if seconds is None:
return None
return arrival_block(
arr_code=arr_ctx.code,
dep_ctx=dep_ctx,
arr_ctx=arr_ctx,
arr_instant=arr_instant,
origin=origin,
destination=home_address,
baseline_seconds=seconds,
config=config,
)
def build_drive_blocks_for_flight(
state: dict,
*,
byair,
maps,
origin: str | None,
home_address: str | None,
config: dict | None = None,
) -> list[DesiredDriveBlock]:
"""Build the airport drive blocks a flight currently warrants. Returns 0–2.
Gated on the flight's `computed_status`: a `to_airport` block while it has
not departed (and an `origin` is resolved), a `from_airport` block once it is
airborne or down (and a `home_address` is configured). Each direction
resolves its airport contexts via `byair.get_airport`, routes the leg via
`maps.travel_time`, and is dropped if its inputs are missing — never raises.
Args:
state: the flight's persisted state record.
byair: a byAir client exposing `get_airport(airport_id)`, or None — then
nothing is built (preserves the never-raises contract).
maps: a Maps client exposing `travel_time(origin=, destination=)`, or
None when no routing key is configured (then nothing is built).
origin: the resolved drive origin for the to_airport leg (the live
location / home), or None — the to_airport block is skipped when it is
None, empty, or whitespace.
home_address: the drive-home destination for the from_airport leg, or
None — the from_airport block is skipped when it is None, empty, or
whitespace.
config: optional `config.json` dict for the clearance overrides.
Returns:
The desired blocks, in to_airport-then-from_airport order. Empty when the
status gates nothing in, a required input is absent, or `maps` is None.
"""
# Either client absent → build nothing. maps is legitimately None when no
# routing key is configured; byair guards the never-raises contract against a
# caller that fumbles the client (it would AttributeError in the airport lookup).
if maps is None or byair is None:
return []
snapshot = state.get("last_snapshot") or {}
status = snapshot.get("computed_status") or "scheduled"
flight_code = state.get("code") or ""
# Normalize endpoints before gating: a blank origin / home_address is absent,
# not a routable string (see `_clean_endpoint`).
origin = _clean_endpoint(origin)
home_address = _clean_endpoint(home_address)
want_departure = status in _TO_AIRPORT_STATUSES and origin is not None
want_arrival = status in _FROM_AIRPORT_STATUSES and home_address is not None
if not want_departure and not want_arrival:
return []
dep_ctx = _safe_airport_context(byair, state.get("dep_airport_id"))
arr_ctx = _safe_airport_context(byair, state.get("arr_airport_id"))
blocks: list[DesiredDriveBlock] = []
if want_departure and origin is not None:
block = _build_departure(
state,
dep_ctx=dep_ctx,
arr_ctx=arr_ctx,
origin=origin,
maps=maps,
flight_code=flight_code,
config=config,
)
if block is not None:
blocks.append(block)
if want_arrival and home_address is not None:
block = _build_arrival(
state,
dep_ctx=dep_ctx,
arr_ctx=arr_ctx,
home_address=home_address,
maps=maps,
config=config,
)
if block is not None:
blocks.append(block)
return blocks
# --- Orchestration: fetch the calendar, plan, execute ------------------------
def _window(block_or_state) -> tuple[datetime, datetime]:
"""The `(start, end)` instants a block occupies, for a desired block or a
parsed `BlockState`. Direction-specific: to_airport runs `[anchor − drive,
anchor]`, from_airport runs `[anchor, anchor + drive]`.
For a desired block the endpoints are read straight off it; for a parsed
`BlockState` they are recomputed from its stored `anchor` + `baseline_seconds`
(what it carries in its description), so the comparison never depends on how
the calendar API echoes the offset.
"""
if isinstance(block_or_state, DesiredDriveBlock):
end = block_or_state.leg_end
return block_or_state.leg_start, end if end is not None else block_or_state.anchor
if block_or_state.direction == "from_airport":
return block_or_state.anchor, block_or_state.anchor + timedelta(
seconds=block_or_state.baseline_seconds
)
return (
block_or_state.anchor - timedelta(seconds=block_or_state.baseline_seconds),
block_or_state.anchor,
)
def _block_window_signature(state) -> str:
"""The `<start>/<end>` signature of a block on the calendar, from its OWN state.
Byte-stable against the same arithmetic `DesiredDriveBlock.signature()` uses
(see `_window`), so a no-op compares equal regardless of the calendar API's
offset formatting.
"""
start, end = _window(state)
return f"{start.isoformat()}/{end.isoformat()}"
def _annotate_signatures(events: list[dict]) -> list[dict]:
"""Attach the state-derived `signature` to each event that is one of our blocks.
`plan_drive_block` compares `event["signature"]` to the desired signature to
decide no-op vs shift. A non-block event (no `<!--fadrive:-->` state) gets no
signature and is ignored by the planner's marker scan.
"""
for event in events:
state = parse_block(event)
if state is not None:
event["signature"] = _block_window_signature(state)
return events
def _existing_block(events: list[dict], flight_id, direction: str):
"""The parsed `BlockState` for this flight+direction among events, or None."""
target = str(flight_id)
for event in events:
state = parse_block(event)
if state is not None and state.flight_id == target and state.direction == direction:
return state
return None
def _window_drift(desired: DesiredDriveBlock, existing) -> timedelta:
"""The larger of the start- and end-instant drift between a desired block and
the block already on the calendar.
Comparing BOTH endpoints is load-bearing: for a `from_airport` block the start
is the (stable) arrival anchor while the end carries the routed drive
duration, so a start-only comparison would read a 20-min → 2-h drive-home
change as zero drift and never shift it.
"""
d_start, d_end = _window(desired)
e_start, e_end = _window(existing)
return max(abs(d_start - e_start), abs(d_end - e_end))
def _fetch_window(states: list[dict], blocks: list[DesiredDriveBlock]) -> tuple[str, str]:
"""RFC 3339 [time_min, time_max] covering every desired block AND every
flight's scheduled times, padded.
Anchoring on the scheduled `dep`/`arr` instants (not only the delayed desired
window) keeps a pre-delay block in range no matter how far the flight has
shifted: the stale block sits within `_FETCH_WINDOW_PAD` of the scheduled
time, so it is fetched and shifted/recreated rather than duplicated.
"""
instants: list[datetime] = []
for state in states:
for key in ("scheduled_dep_time", "scheduled_arr_time"):
dt = _parse_instant(state.get(key))
if dt is not None:
instants.append(dt)
for block in blocks:
instants.append(block.leg_start)
instants.append(block.leg_end if block.leg_end is not None else block.anchor)
lo = (min(instants) - _FETCH_WINDOW_PAD).astimezone(timezone.utc)
hi = (max(instants) + _FETCH_WINDOW_PAD).astimezone(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"
return lo.strftime(fmt), hi.strftime(fmt)
def _fetch_block_events(
composio, *, calendar_id: str, states: list[dict], blocks: list[DesiredDriveBlock]
) -> list[dict]:
"""Fetch the primary-calendar events in the window spanning the desired blocks
and the flights' scheduled times (see `_fetch_window`).
Returns the raw Composio events (description intact, so `parse_block` reads
the `<!--fadrive:-->` state), each annotated with its state-derived signature.
"""
time_min, time_max = _fetch_window(states, blocks)
raw = composio.find_events(
_find_events_args(calendar_id=calendar_id, time_min=time_min, time_max=time_max)
)
return _annotate_signatures(_items(raw))
def _execute_op(op: dict, *, composio) -> None:
"""Execute one planner op against the calendar. create / update only.
A shift (`update`) is a recreate-then-delete, so the replacement always goes
through `build_block_args`' timezone-aware create path rather than a PATCH
that would re-introduce the offset-as-UTC ambiguity (#83). Create FIRST so the
old block is never removed before its replacement exists (no gap on a
transient create failure — that raises before any delete, leaving the old
block intact for the next cycle). Then delete the old block; if that delete
fails for a real reason, roll back the just-created replacement so the cycle
never leaves a duplicate, and re-raise so the op defers. A delete that 404s
(old already gone) leaves the new block standing alone — an idempotent success.
"""
if op["op"] == "create":
composio.create_event(op["create_args"])
return
if op["op"] == "update":
created = composio.create_event(op["create_args"])
try:
composio.delete_event({"calendar_id": op["calendar_id"], "event_id": op["event_id"]})
except (ComposioError, urllib.error.URLError) as exc:
if getattr(exc, "status_code", None) == 404:
return # old already gone — the replacement stands alone
new_id = _created_event_id(created)
if new_id is not None:
try:
composio.delete_event({"calendar_id": op["calendar_id"], "event_id": new_id})
except (ComposioError, urllib.error.URLError) as rollback_exc:
# Rollback failed too — a duplicate may remain until the next
# cycle reconciles it. Log it, but re-raise the ORIGINAL delete
# failure so the caller records the real reason, not this one.
print(
f"flight-assist airport-drive: rollback of replacement {new_id} failed "
f"after the old-block delete failed; a duplicate may remain until the "
f"next cycle: {rollback_exc}",
file=sys.stderr,
)
raise
return
raise ValueError(f"airport_drive_reconcile: unexpected op {op['op']!r}")
def run_airport_drive_reconcile(
states: list[dict],
*,
composio,
byair,
maps,
origin: str | None,
home_address: str | None,
calendar_id: str = PRIMARY_CALENDAR_ID,
config: dict | None = None,
) -> dict:
"""Reconcile every active flight's airport drive blocks against the calendar.
For each state in `states`, assembles the blocks it currently warrants
(`build_drive_blocks_for_flight`), then — across all of them — fetches the
block calendar once over the spanning window, and per block runs
`plan_drive_block` and executes the resulting create / shift. A shift whose
re-routed leave-by drifts less than `_REANCHOR_THRESHOLD` from the block
already on the calendar is suppressed (anti-thrash, #90 §7).
Per-op (create / delete) failures are collected, not raised — one bad write
defers that op to the next cycle. The one-shot calendar FETCH is the
exception: a `find_events` failure propagates (there is nothing to reconcile
against without it), matching `calendar_reconcile`. Returns a summary
`{status, planned, executed, suppressed, failed}`.
Args:
states: the active flights' state records.
composio: a Composio client (`find_events` / `create_event` /
`delete_event`).
byair / maps: the airport-context and routing clients (see
`build_drive_blocks_for_flight`); either None builds nothing.
origin: the resolved to_airport drive origin (live location / home).
home_address: the from_airport drive destination.
calendar_id: the calendar the blocks live on (primary).
config: optional `config.json` for the clearance overrides.
"""
desired: list[tuple[dict, DesiredDriveBlock]] = []
for state in states:
for block in build_drive_blocks_for_flight(
state, byair=byair, maps=maps, origin=origin, home_address=home_address, config=config
):
desired.append((state, block))
if not desired:
return {"status": "ok", "planned": 0, "executed": 0, "suppressed": 0, "failed": []}
events = _fetch_block_events(
composio,
calendar_id=calendar_id,
states=[state for state, _ in desired],
blocks=[block for _, block in desired],
)
planned = 0
executed = 0
suppressed = 0
failed: list[dict] = []
for state, block in desired:
flight_id = state["flight_id"]
flight_code = state.get("code") or ""
existing = _existing_block(events, flight_id, block.direction)
# Anti-thrash: an existing block whose full window (start AND end) is
# within the threshold of the freshly-routed one stays put — skip before
# planning a shift.
if existing is not None and _window_drift(block, existing) < _REANCHOR_THRESHOLD:
suppressed += 1
continue
ops = plan_drive_block(
flight_id=flight_id,
flight_code=flight_code,
desired=block,
events=events,
calendar_id=calendar_id,
)
for op in ops:
planned += 1
try:
_execute_op(op, composio=composio)
except (ComposioError, urllib.error.URLError) as exc:
print(
f"flight-assist airport-drive: op {op['op']}/{op['kind']} for flight "
f"{flight_id} failed — deferred, retried next cycle. If this repeats, check "
f"Composio/Google Calendar connectivity and credentials (COMPOSIO_API_KEY / "
f"COMPOSIO_USER_ID). Cause: {exc}",
file=sys.stderr,
)
failed.append({"flight_id": flight_id, "op": op["op"], "kind": op["kind"]})
continue
executed += 1
return {
"status": "ok",
"planned": planned,
"executed": executed,
"suppressed": suppressed,
"failed": failed,
}
# --- Wake-cycle entry point (env + state glue) -------------------------------
# Per-call client timeouts for the wake cycle. Unlike the precheck (bounded by
# the agent-runner's 30s kill), the reconcile runs on the wake, so the client
# defaults are fine; pinned here only to keep one slow upstream from stalling
# the whole pass.
_BYAIR_CALL_TIMEOUT_SECONDS = 8.0
_MAPS_CALL_TIMEOUT_SECONDS = 8.0
def _idle_summary() -> dict:
"""A fresh zero-op summary (a new `failed` list each call, never shared)."""
return {"status": "ok", "planned": 0, "executed": 0, "suppressed": 0, "failed": []}
def _maybe_byair_client() -> ByAirClient | None:
"""A byAir client when `BYAIR_MCP_URL` is set, else None (pass builds nothing)."""
if not os.environ.get("BYAIR_MCP_URL"):
return None
return ByAirClient.from_env(timeout=_BYAIR_CALL_TIMEOUT_SECONDS)
def _maybe_maps_client() -> MapsClient | None:
"""A Maps client when `GOOGLE_MAPS_API_KEY` is set, else None (no routing)."""
if not os.environ.get("GOOGLE_MAPS_API_KEY"):
return None
return MapsClient.from_env(timeout=_MAPS_CALL_TIMEOUT_SECONDS)
def run_airport_drive_pass(composio, *, now: datetime) -> dict:
"""The wake-cycle airport-drive reconcile: resolve inputs from env + state,
then run `run_airport_drive_reconcile`.
Reads the tile config (`home_address`), resolves the live drive origin
(`state.resolve_live_origin` — the same ladder the precheck uses), constructs
the byAir + Maps clients from the environment, loads the active flights'
states, and reconciles their airport drive blocks on the primary calendar.
Returns the reconcile summary, or an idle summary when routing is unavailable
(no Maps key, no byAir URL, or no tracked flights) — the airport blocks are an
additive capability that simply stays dormant without its inputs, never an
error for the wake cycle.
"""
maps = _maybe_maps_client()
byair = _maybe_byair_client()
if maps is None or byair is None:
return _idle_summary()
config = read_config() or {}
home_address = config.get("home_address")
origin = resolve_live_origin(home_address, now=now)
states = [
state for fid in read_active_flights() if (state := read_flight_state(fid)) is not None
]
if not states:
return _idle_summary()
return run_airport_drive_reconcile(
states,
composio=composio,
byair=byair,
maps=maps,
origin=origin,
home_address=home_address,
config=config,
)skills
check-travel-bookings
drive-planner
drive-planner-recheck
flight-assist
references
nightly-travel-sync
sync-tripit