booking-agent-cli 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/PKG-INFO +2 -2
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/README.md +1 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/pyproject.toml +1 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/__init__.py +1 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/backend/live.py +16 -6
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/defaults.env +6 -0
- booking_agent_cli-0.9.0/src/booking_agent/faults.py +154 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/observability.py +11 -4
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_profile.py +3 -2
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/__init__.py +1 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/booking_summary.py +173 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/bundled_skill.md +59 -58
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/calendar_event.py +4 -12
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/calendar_preference.py +9 -23
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/flow.py +801 -107
- booking_agent_cli-0.9.0/src/booking_agent_cli/fault_report_schema.py +67 -0
- booking_agent_cli-0.9.0/src/booking_agent_cli/fault_reports.py +252 -0
- booking_agent_cli-0.9.0/src/booking_agent_cli/gates.py +82 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/geo_eligibility.py +1 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/identity.py +7 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/main.py +15 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/output.py +6 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/session.py +58 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/support_bundle_schema.py +3 -4
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/support_report.py +40 -8
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/conftest.py +14 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/test_timezone_booking_e2e.py +6 -3
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/test_ucp_skill_flow_e2e.py +40 -9
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_booking_rejection.py +11 -5
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_connector.py +1 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_flow.py +37 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_preference.py +5 -5
- booking_agent_cli-0.9.0/tests/integration/test_cli_enforced_gates.py +660 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_declined_spt.py +1 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_gates.py +98 -69
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_home_visit_address.py +4 -3
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_install_integrity.py +2 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_link_auth.py +34 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_skill_integrity.py +2 -1
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_support_report.py +2 -2
- booking_agent_cli-0.9.0/tests/unit/test_fault_reports.py +275 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/.gitignore +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/authorization.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/backend/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/filter.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/google.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/scope.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/circuit_breaker.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/config.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/discovery.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/headless.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/http_deadlines.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/http_retry.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/http_trace.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/merchant_pacing.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/models/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/models/platform.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/models/wire.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/namespace.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/platform_profile.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/pop.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/profile_cache.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/registry_url.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/rest_client.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/secure_file.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/settings.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/signals.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/step_timing.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_capabilities.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_checkout.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_negotiation.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent/usp_methods.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/activity_log.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/booking_poll.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/calendar_connection.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/catalog.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/config.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/runtime.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/update.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/config_loader.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/engine.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/flow_timings.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/geocode.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/install_integrity.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/link_cli.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/payment_helpers.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/payment_token.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/provenance.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/reschedule.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/runtime.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/runtime_prepare.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/search_area.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/skill_bundle.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/skill_integrity.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/timezone_context.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/updates.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/.env.tests +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/test_latency_soak.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/test_staging_registry_integration.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/e2e/ucp_native.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_activity_log.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_at_rest_hygiene.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_bulk_cancel.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_connection.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_checkout_signals.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_circuit_breaker.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_config.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_correlation_flow.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_discover_registry_rejection.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_evidence.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_reschedule.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_step_timings.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_google_calendar_oauth.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_http_trace.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_identity_logout.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_lazy_oauth.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_link_cli_spt.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_namespace.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_oauth_listener.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_oauth_metadata.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_platform_key_pop.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_platform_profile.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_pop_keystore.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_profile_cache.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_regression_index.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_request_retry_semantics.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_runtime_prepare.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_search_area.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_session_compat.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_session_retention.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_skill_portability.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_timezone_context.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_cancel_result.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_profile.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_update_command.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/integration/test_wheel_contents.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/SOURCE +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/availability.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/booking.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/catalog.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/paid_bookings.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/profile.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/registry.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/rest_common.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/usp.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/waitlist.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/webhook_event.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/profiles.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/services.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/backend.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/link_cli.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/mock_calendar.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/pop_business.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/pop_transport.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/ucp_checkout_store.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/mock/ucp_server.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/offline_stack/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/support/offline_stack/stack.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/__init__.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_authorization_policy.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_booking_poll_credentials.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_booking_request_error.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_calendar_filter.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_catalog.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_check_version.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_conditional_writes.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_delivery_address.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_discovery.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_geo_eligibility.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_geocode.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_headless.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_http_deadlines.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_http_retry.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_live_search_registry.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_merchant_pacing.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_mock_support.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_money_path_events.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_observability.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_outbound_member_names.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_package_layering.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_pay_at_service.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_provenance.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_search_filters.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_search_pricing.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_search_responses.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_url.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_release_notes.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_rest_client.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_scoped_method_authorization.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_session_credentials.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_spec_conformance.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_step_timing.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_capabilities.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_negotiation.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_updates.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/unit/test_wire_model_conformance.py +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/010-alg-none.json +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/README.md +0 -0
- {booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/SOURCE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: booking-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/wix/booking-agent
|
|
6
6
|
Project-URL: Documentation, https://github.com/wix/booking-agent/blob/master/booking-agent/README.md
|
|
@@ -78,7 +78,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
78
78
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
81
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.57.0/booking-agent/skills/booking-agent/SKILL.md
|
|
82
82
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
83
83
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
84
84
|
```
|
|
@@ -36,7 +36,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
36
36
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
39
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.57.0/booking-agent/skills/booking-agent/SKILL.md
|
|
40
40
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
41
41
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
42
42
|
```
|
|
@@ -33,6 +33,7 @@ from ..rest_client import (
|
|
|
33
33
|
USPTokenStore,
|
|
34
34
|
)
|
|
35
35
|
from ..headless import attach_registry_hit, mark_live_catalog_service, service_from_registry_hit
|
|
36
|
+
from ..faults import record_silent_fault
|
|
36
37
|
from ..authorization import BookingScopedCredential, evaluate, resolve_posture
|
|
37
38
|
from ..pop import PopKey, PopKeyStore
|
|
38
39
|
from ..ucp_profile import UcpNativeContext, fetch_and_resolve_business_profile
|
|
@@ -115,39 +116,41 @@ class LiveUSPBackend(USPBackend):
|
|
|
115
116
|
decision=decision,
|
|
116
117
|
)
|
|
117
118
|
|
|
119
|
+
# Discovery absorbs these failures, so they stay at debug level: a warning reaches
|
|
120
|
+
# stderr, which agent hosts merge into the stdout JSON they parse.
|
|
118
121
|
@staticmethod
|
|
119
122
|
def _log_profile_failure(profile_url: str, exc: BaseException) -> None:
|
|
120
123
|
if is_rate_limited(exc):
|
|
121
|
-
logger.
|
|
124
|
+
logger.debug(
|
|
122
125
|
"Profile resolution rate limited (HTTP 429) for %s",
|
|
123
126
|
profile_url,
|
|
124
127
|
)
|
|
125
128
|
return
|
|
126
129
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
127
|
-
logger.
|
|
130
|
+
logger.debug(
|
|
128
131
|
"Profile resolution HTTP %s for %s",
|
|
129
132
|
exc.response.status_code,
|
|
130
133
|
profile_url,
|
|
131
134
|
)
|
|
132
135
|
return
|
|
133
|
-
logger.
|
|
136
|
+
logger.debug("Profile resolution failed for %s: %s: %s", profile_url, type(exc).__name__, exc)
|
|
134
137
|
|
|
135
138
|
@staticmethod
|
|
136
139
|
def _log_service_failure(service_id: str, exc: BaseException) -> None:
|
|
137
140
|
if is_rate_limited(exc):
|
|
138
|
-
logger.
|
|
141
|
+
logger.debug(
|
|
139
142
|
"Service catalog fetch rate limited (HTTP 429) for %s",
|
|
140
143
|
service_id,
|
|
141
144
|
)
|
|
142
145
|
return
|
|
143
146
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
144
|
-
logger.
|
|
147
|
+
logger.debug(
|
|
145
148
|
"Service catalog fetch HTTP %s for %s",
|
|
146
149
|
exc.response.status_code,
|
|
147
150
|
service_id,
|
|
148
151
|
)
|
|
149
152
|
return
|
|
150
|
-
logger.
|
|
153
|
+
logger.debug("Service fetch failed: %s: %s", service_id, type(exc).__name__)
|
|
151
154
|
|
|
152
155
|
# -- Registry-level ------------------------------------------------------
|
|
153
156
|
|
|
@@ -184,6 +187,9 @@ class LiveUSPBackend(USPBackend):
|
|
|
184
187
|
self._log_profile_failure(profile_url, exc)
|
|
185
188
|
if is_rate_limited(exc):
|
|
186
189
|
raise
|
|
190
|
+
record_silent_fault(
|
|
191
|
+
"profile_resolve", exc, profile_url=profile_url, business_id=biz_id,
|
|
192
|
+
)
|
|
187
193
|
|
|
188
194
|
async def search_registry(self, intent: ServiceSearchIntent) -> List[USPService]:
|
|
189
195
|
if not self._cfg.usp_registry_url:
|
|
@@ -289,6 +295,10 @@ class LiveUSPBackend(USPBackend):
|
|
|
289
295
|
if is_rate_limited(exc):
|
|
290
296
|
rate_limited_exc = exc
|
|
291
297
|
raise
|
|
298
|
+
record_silent_fault(
|
|
299
|
+
"catalog_enrich", exc, profile_url=profile_url,
|
|
300
|
+
business_id=biz_id, service_id=r.service_id,
|
|
301
|
+
)
|
|
292
302
|
return attach_registry_hit(
|
|
293
303
|
service_from_registry_hit(r), r, rank=rank,
|
|
294
304
|
)
|
|
@@ -43,6 +43,12 @@ BOOKING_AGENT_DISCOVERY_CONCURRENCY=8
|
|
|
43
43
|
BOOKING_AGENT_PROFILE_CACHE_TTL_SECONDS=900
|
|
44
44
|
# 1 writes a redacted request log to <session dir>/http_trace.log.
|
|
45
45
|
BOOKING_AGENT_HTTP_TRACE=0
|
|
46
|
+
# 1 writes money-path JSON event lines to stderr. Off by default because many
|
|
47
|
+
# agent hosts merge stderr into the stdout JSON they parse.
|
|
48
|
+
BOOKING_AGENT_STDERR_EVENTS=0
|
|
49
|
+
# 0 stops automatic fault reports: faults the user never saw, about a business's
|
|
50
|
+
# profile or catalog or the assistant's own code, sent with no user data.
|
|
51
|
+
BOOKING_AGENT_FAULT_REPORTS=1
|
|
46
52
|
|
|
47
53
|
# --- Address -------------------------------------------------------------------
|
|
48
54
|
BOOKING_AGENT_GEOCODER_URL=https://nominatim.openstreetmap.org/search
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Faults the client absorbed, so the user never saw them.
|
|
2
|
+
|
|
3
|
+
Code that catches an exception and carries on (a business profile that cannot be
|
|
4
|
+
read during discovery, a catalog entry that cannot be fetched) calls
|
|
5
|
+
``record_silent_fault``. Warnings logged by the allowed modules during a command are
|
|
6
|
+
collected the same way. The CLI decides at the end of the command whether these
|
|
7
|
+
become automatic fault reports; nothing here does I/O.
|
|
8
|
+
|
|
9
|
+
Only structural facts are kept: the step, the exception type, a code-authored log
|
|
10
|
+
format string, and public identifiers of the business involved. Exception messages
|
|
11
|
+
and log arguments are never kept, because they can carry anything.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
from urllib.parse import urlsplit
|
|
19
|
+
|
|
20
|
+
#: Steps whose outcome never depends on the user's own accounts or data.
|
|
21
|
+
STEPS = frozenset({
|
|
22
|
+
"registry_search",
|
|
23
|
+
"profile_resolve",
|
|
24
|
+
"catalog_enrich",
|
|
25
|
+
"availability_prefetch",
|
|
26
|
+
"platform_profile",
|
|
27
|
+
"cli",
|
|
28
|
+
})
|
|
29
|
+
#: Loggers whose warnings count as degraded results. Link, payment, identity, buyer
|
|
30
|
+
#: details, and Google Calendar code is not here: their faults stay on the consented path.
|
|
31
|
+
WARNING_LOGGERS = (
|
|
32
|
+
"booking_agent.backend.live",
|
|
33
|
+
"booking_agent.discovery",
|
|
34
|
+
"booking_agent.headless",
|
|
35
|
+
"booking_agent.ucp_profile",
|
|
36
|
+
"booking_agent.profile_cache",
|
|
37
|
+
)
|
|
38
|
+
_MAX_RECORDED = 50
|
|
39
|
+
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
|
40
|
+
|
|
41
|
+
_recorded: List[Dict[str, Any]] = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _reason(exc: Optional[BaseException]) -> Optional[str]:
|
|
45
|
+
if exc is None:
|
|
46
|
+
return None
|
|
47
|
+
response = getattr(exc, "response", None)
|
|
48
|
+
status = getattr(response, "status_code", None)
|
|
49
|
+
if isinstance(status, int):
|
|
50
|
+
return f"http_{status}"
|
|
51
|
+
from booking_agent.ucp_profile import UcpProfileValidationError
|
|
52
|
+
|
|
53
|
+
if isinstance(exc, UcpProfileValidationError) and exc.args and isinstance(exc.args[0], str):
|
|
54
|
+
# Validation messages are written in this package and name only profile fields.
|
|
55
|
+
slug = _SLUG_RE.sub("_", exc.args[0].lower()).strip("_")
|
|
56
|
+
return slug[:64] or None
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _subject(profile_url: Optional[str], **ids: Any) -> Dict[str, Any]:
|
|
61
|
+
subject: Dict[str, Any] = {}
|
|
62
|
+
if profile_url:
|
|
63
|
+
try:
|
|
64
|
+
parts = urlsplit(profile_url)
|
|
65
|
+
except ValueError:
|
|
66
|
+
parts = None
|
|
67
|
+
if parts is not None and parts.scheme in ("http", "https") and parts.hostname:
|
|
68
|
+
subject["host"] = parts.hostname
|
|
69
|
+
subject["path"] = parts.path or "/"
|
|
70
|
+
for key, value in ids.items():
|
|
71
|
+
if value is not None:
|
|
72
|
+
subject[key] = value
|
|
73
|
+
return subject
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def record_silent_fault(
|
|
77
|
+
step: str,
|
|
78
|
+
exc: Optional[BaseException] = None,
|
|
79
|
+
*,
|
|
80
|
+
profile_url: Optional[str] = None,
|
|
81
|
+
business_id: Optional[str] = None,
|
|
82
|
+
service_id: Optional[str] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Note a fault this code absorbed. Never raises."""
|
|
85
|
+
try:
|
|
86
|
+
if len(_recorded) >= _MAX_RECORDED:
|
|
87
|
+
return
|
|
88
|
+
response = getattr(exc, "response", None)
|
|
89
|
+
status = getattr(response, "status_code", None)
|
|
90
|
+
_recorded.append({
|
|
91
|
+
"fault_class": "absorbed_exception",
|
|
92
|
+
"step": step if step in STEPS else "cli",
|
|
93
|
+
"exception_type": _qualname(exc),
|
|
94
|
+
"message_template": None,
|
|
95
|
+
"reason_code": _reason(exc),
|
|
96
|
+
"subject": _subject(
|
|
97
|
+
profile_url,
|
|
98
|
+
business_id=business_id,
|
|
99
|
+
service_ids=[service_id] if service_id else None,
|
|
100
|
+
http_status=status if isinstance(status, int) else None,
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _qualname(exc: Optional[BaseException]) -> Optional[str]:
|
|
108
|
+
if exc is None:
|
|
109
|
+
return None
|
|
110
|
+
cls = type(exc)
|
|
111
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _WarningCollector(logging.Handler):
|
|
115
|
+
"""Keeps the format string of each warning; the formatted text is never stored."""
|
|
116
|
+
|
|
117
|
+
def __init__(self) -> None:
|
|
118
|
+
super().__init__(level=logging.WARNING)
|
|
119
|
+
|
|
120
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
121
|
+
try:
|
|
122
|
+
if not record.name.startswith(WARNING_LOGGERS) or len(_recorded) >= _MAX_RECORDED:
|
|
123
|
+
return
|
|
124
|
+
exc = record.exc_info[1] if record.exc_info else None
|
|
125
|
+
_recorded.append({
|
|
126
|
+
"fault_class": "degraded_result",
|
|
127
|
+
"step": "cli",
|
|
128
|
+
"exception_type": _qualname(exc),
|
|
129
|
+
"message_template": record.msg if isinstance(record.msg, str) else None,
|
|
130
|
+
"reason_code": None,
|
|
131
|
+
"subject": {"logger": record.name},
|
|
132
|
+
})
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_collector: Optional[_WarningCollector] = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def install_warning_collector() -> None:
|
|
141
|
+
"""Collect allowed warnings instead of printing them to stderr (idempotent)."""
|
|
142
|
+
global _collector
|
|
143
|
+
if _collector is not None:
|
|
144
|
+
return
|
|
145
|
+
_collector = _WarningCollector()
|
|
146
|
+
for name in ("booking_agent", "booking_agent_cli"):
|
|
147
|
+
logging.getLogger(name).addHandler(_collector)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def drain() -> List[Dict[str, Any]]:
|
|
151
|
+
"""Every fault recorded so far, cleared."""
|
|
152
|
+
out = list(_recorded)
|
|
153
|
+
_recorded.clear()
|
|
154
|
+
return out
|
|
@@ -9,6 +9,8 @@ from typing import Any, Callable, Dict, Optional
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
|
+
from booking_agent import settings
|
|
13
|
+
|
|
12
14
|
# Wire name for the flow join key. Optional on the USP HTTP binding (Track S of
|
|
13
15
|
# spec issue #160): send when bound, never require it on responses, never fail a
|
|
14
16
|
# call because a peer omitted or ignored it.
|
|
@@ -35,9 +37,14 @@ _FORBIDDEN_FIELD_KEYS = frozenset({
|
|
|
35
37
|
})
|
|
36
38
|
|
|
37
39
|
_correlation_id: ContextVar[Optional[str]] = ContextVar("booking_agent_correlation_id", default=None)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_sink(record: Dict[str, Any]) -> None:
|
|
43
|
+
if settings.get_bool("BOOKING_AGENT_STDERR_EVENTS"):
|
|
44
|
+
sys.stderr.write(json.dumps(record, default=str) + "\n")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_event_sink: Callable[[Dict[str, Any]], None] = _default_sink
|
|
41
48
|
|
|
42
49
|
|
|
43
50
|
def set_event_sink(sink: Callable[[Dict[str, Any]], None]) -> None:
|
|
@@ -49,7 +56,7 @@ def set_event_sink(sink: Callable[[Dict[str, Any]], None]) -> None:
|
|
|
49
56
|
def reset_event_sink() -> None:
|
|
50
57
|
"""Restore the default stderr event sink."""
|
|
51
58
|
global _event_sink
|
|
52
|
-
_event_sink =
|
|
59
|
+
_event_sink = _default_sink
|
|
53
60
|
|
|
54
61
|
|
|
55
62
|
def new_correlation_id() -> str:
|
|
@@ -169,10 +169,11 @@ class UcpNativeContext(BaseModel):
|
|
|
169
169
|
continue
|
|
170
170
|
if "authorization" in binding:
|
|
171
171
|
logger.warning(
|
|
172
|
-
|
|
172
|
+
"%s binding declares a sibling 'authorization' member, "
|
|
173
173
|
"which USP prohibits; the policy MUST be published as "
|
|
174
174
|
"config.authorization. Treating this business as publishing no "
|
|
175
|
-
"policy."
|
|
175
|
+
"policy.",
|
|
176
|
+
SVC_SERVICES,
|
|
176
177
|
)
|
|
177
178
|
misplaced = True
|
|
178
179
|
continue
|
{booking_agent_cli-0.8.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/booking_summary.py
RENAMED
|
@@ -6,6 +6,7 @@ address, and price.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
from typing import Any, Dict, List, Optional
|
|
10
11
|
|
|
11
12
|
from . import timezone_context as tz_context
|
|
@@ -114,6 +115,174 @@ def price_text(session: Any, price_info: Dict[str, Any]) -> Optional[str]:
|
|
|
114
115
|
return f"{price}, charged now to your Link wallet"
|
|
115
116
|
|
|
116
117
|
|
|
118
|
+
_ISO_DURATION_RE = re.compile(
|
|
119
|
+
r"^P(?:(?P<days>\d+)D)?(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?)?$"
|
|
120
|
+
)
|
|
121
|
+
# Links a checkout returns that are actions, not terms the business publishes.
|
|
122
|
+
_ACTION_LINK_TYPES = frozenset({"continue"})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _duration_text(value: Any) -> Optional[str]:
|
|
126
|
+
"""``PT24H`` -> ``24 hours``; anything else is returned as the business wrote it."""
|
|
127
|
+
if not isinstance(value, str) or not value.strip():
|
|
128
|
+
return None
|
|
129
|
+
match = _ISO_DURATION_RE.match(value.strip().upper())
|
|
130
|
+
if not match or not any(match.groupdict().values()):
|
|
131
|
+
return value.strip()
|
|
132
|
+
parts = []
|
|
133
|
+
for unit in ("days", "hours", "minutes"):
|
|
134
|
+
amount = match.group(unit)
|
|
135
|
+
if amount and int(amount):
|
|
136
|
+
word = unit if int(amount) != 1 else unit[:-1]
|
|
137
|
+
parts.append(f"{int(amount)} {word}")
|
|
138
|
+
return " ".join(parts) or "0 minutes"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _fee_text(amount: Any, live: Dict[str, Any]) -> Optional[str]:
|
|
142
|
+
if not isinstance(amount, int) or amount <= 0:
|
|
143
|
+
return None
|
|
144
|
+
from .catalog import _format_price_display
|
|
145
|
+
|
|
146
|
+
pricing = live.get("pricing") if isinstance(live.get("pricing"), dict) else {}
|
|
147
|
+
return _format_price_display(amount, pricing.get("currency")) or str(amount)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def service_policy_lines(live_service: Any) -> List[str]:
|
|
151
|
+
"""The cancellation and rescheduling policies the business published for the service.
|
|
152
|
+
|
|
153
|
+
``allowed`` is required, and a business with no policy configured still sends
|
|
154
|
+
``allowed: true``, so a policy that is only ``allowed: true`` with no window, fee, or
|
|
155
|
+
limit says nothing the business chose and yields no line.
|
|
156
|
+
"""
|
|
157
|
+
live = live_service if isinstance(live_service, dict) else {}
|
|
158
|
+
policies = live.get("policies") if isinstance(live.get("policies"), dict) else {}
|
|
159
|
+
lines: List[str] = []
|
|
160
|
+
cancel = policies.get("cancellation")
|
|
161
|
+
if isinstance(cancel, dict) and cancel.get("allowed") is not None:
|
|
162
|
+
if cancel.get("allowed") is False:
|
|
163
|
+
text = "Cancellation is not allowed"
|
|
164
|
+
else:
|
|
165
|
+
parts = []
|
|
166
|
+
free = _duration_text(cancel.get("free_cancellation_until"))
|
|
167
|
+
if free:
|
|
168
|
+
parts.append(f"free up to {free} before the appointment")
|
|
169
|
+
fee = _fee_text(cancel.get("late_cancellation_fee"), live)
|
|
170
|
+
if fee:
|
|
171
|
+
parts.append(f"a {fee} fee after that")
|
|
172
|
+
closed = _duration_text(cancel.get("no_cancellation_after"))
|
|
173
|
+
if closed:
|
|
174
|
+
parts.append(f"no cancellation within {closed} of the appointment")
|
|
175
|
+
text = f"Cancellation allowed, {', '.join(parts)}" if parts else None
|
|
176
|
+
if text:
|
|
177
|
+
lines.append(f"Cancellation policy: {text}.")
|
|
178
|
+
resched = policies.get("rescheduling")
|
|
179
|
+
if isinstance(resched, dict) and resched.get("allowed") is not None:
|
|
180
|
+
if resched.get("allowed") is False:
|
|
181
|
+
text = "Rescheduling is not allowed"
|
|
182
|
+
else:
|
|
183
|
+
parts = []
|
|
184
|
+
free = _duration_text(resched.get("free_reschedule_until"))
|
|
185
|
+
if free:
|
|
186
|
+
parts.append(f"free up to {free} before the appointment")
|
|
187
|
+
limit = resched.get("max_reschedules")
|
|
188
|
+
if isinstance(limit, int) and limit >= 0:
|
|
189
|
+
parts.append(f"at most {limit} time{'s' if limit != 1 else ''}")
|
|
190
|
+
fee = _fee_text(resched.get("fee"), live)
|
|
191
|
+
if fee:
|
|
192
|
+
parts.append(f"a {fee} fee")
|
|
193
|
+
text = f"Rescheduling allowed, {', '.join(parts)}" if parts else None
|
|
194
|
+
if text:
|
|
195
|
+
lines.append(f"Rescheduling policy: {text}.")
|
|
196
|
+
return lines
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _http_url(value: Any) -> Optional[str]:
|
|
200
|
+
if not isinstance(value, str):
|
|
201
|
+
return None
|
|
202
|
+
url = value.strip()
|
|
203
|
+
return url if url.startswith(("https://", "http://")) else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _link_label(kind: str, title: Any) -> str:
|
|
207
|
+
if isinstance(title, str) and title.strip():
|
|
208
|
+
return title.strip()
|
|
209
|
+
words = kind.replace("_", " ").replace("-", " ").strip()
|
|
210
|
+
return (words[:1].upper() + words[1:]) if words else "Link"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def extract_policy_links(links: Any) -> List[Dict[str, str]]:
|
|
214
|
+
"""Terms and policy links from a checkout: no ``rel=continue`` actions, no duplicates."""
|
|
215
|
+
out: List[Dict[str, str]] = []
|
|
216
|
+
seen: set = set()
|
|
217
|
+
for link in links or []:
|
|
218
|
+
if not isinstance(link, dict):
|
|
219
|
+
continue
|
|
220
|
+
kind = str(link.get("type") or link.get("rel") or "").strip().lower()
|
|
221
|
+
if kind in _ACTION_LINK_TYPES:
|
|
222
|
+
continue
|
|
223
|
+
url = _http_url(link.get("url") or link.get("href"))
|
|
224
|
+
if url is None or url in seen:
|
|
225
|
+
continue
|
|
226
|
+
seen.add(url)
|
|
227
|
+
out.append({"type": kind or "link", "label": _link_label(kind, link.get("title")), "url": url})
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _message_text(message: Dict[str, Any], zone: Optional[str]) -> Optional[str]:
|
|
232
|
+
description = message.get("description")
|
|
233
|
+
text = description.get("plain") if isinstance(description, dict) else None
|
|
234
|
+
for key in ("content", "text", "message", "title", "name"):
|
|
235
|
+
if isinstance(text, str) and text.strip():
|
|
236
|
+
break
|
|
237
|
+
text = message.get(key)
|
|
238
|
+
if not isinstance(text, str) or not text.strip():
|
|
239
|
+
return None
|
|
240
|
+
return tz_context.localize_instants_in_text(text.strip(), zone)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def collect_provider_policies(session: Any) -> Dict[str, Any]:
|
|
244
|
+
"""Everything the business published for the user to see before the booking is final.
|
|
245
|
+
|
|
246
|
+
``lines`` are the service's cancellation and rescheduling policies, the checkout's policy
|
|
247
|
+
texts, payment terms, and disclosures; ``links`` are the checkout's policy links (never
|
|
248
|
+
``rel=continue``). ``digest`` covers ``lines`` only: the user accepts the lines with the
|
|
249
|
+
booking summary, and links a checkout adds later are shown without a new question, so a
|
|
250
|
+
checkout re-asks only when it adds lines. The business website the CLI derives from the
|
|
251
|
+
profile and any text the CLI writes are not included; with no lines ``digest`` is None.
|
|
252
|
+
"""
|
|
253
|
+
zone = session.business.timezone if getattr(session, "business", None) else None
|
|
254
|
+
lines = service_policy_lines(getattr(session, "live_service", None))
|
|
255
|
+
links = extract_policy_links(getattr(session, "ucp_checkout_links", None))
|
|
256
|
+
seen_urls = {link["url"] for link in links}
|
|
257
|
+
for policy in getattr(session, "ucp_checkout_policies", None) or []:
|
|
258
|
+
if not isinstance(policy, dict):
|
|
259
|
+
continue
|
|
260
|
+
url = _http_url(policy.get("url") or policy.get("href"))
|
|
261
|
+
if url and url not in seen_urls:
|
|
262
|
+
seen_urls.add(url)
|
|
263
|
+
kind = str(policy.get("type") or "policy").strip().lower()
|
|
264
|
+
links.append({"type": kind, "label": _link_label(kind, policy.get("title")), "url": url})
|
|
265
|
+
text = _message_text(policy, zone)
|
|
266
|
+
if text and not url:
|
|
267
|
+
lines.append(f"Policy: {text}")
|
|
268
|
+
for schedule in getattr(session, "payment_schedules", None) or []:
|
|
269
|
+
if isinstance(schedule, dict):
|
|
270
|
+
text = _message_text({"description": schedule.get("description")}, zone)
|
|
271
|
+
if text:
|
|
272
|
+
lines.append(f"Payment terms: {text}")
|
|
273
|
+
for disclosure in getattr(session, "payment_disclosures", None) or []:
|
|
274
|
+
if isinstance(disclosure, dict):
|
|
275
|
+
text = _message_text(disclosure, zone)
|
|
276
|
+
if text:
|
|
277
|
+
lines.append(text)
|
|
278
|
+
digest = None
|
|
279
|
+
if lines:
|
|
280
|
+
from .gates import digest as _digest
|
|
281
|
+
|
|
282
|
+
digest = _digest({"lines": lines})
|
|
283
|
+
return {"lines": lines, "links": links, "digest": digest}
|
|
284
|
+
|
|
285
|
+
|
|
117
286
|
def booking_recap_lines(
|
|
118
287
|
session: Any, price_info: Dict[str, Any], *, dual: bool,
|
|
119
288
|
) -> List[str]:
|
|
@@ -131,6 +300,10 @@ def booking_recap_lines(
|
|
|
131
300
|
):
|
|
132
301
|
if value:
|
|
133
302
|
lines.append(f"{label}: {value}")
|
|
303
|
+
mismatch = getattr(session, "selection_mismatch", None)
|
|
304
|
+
if mismatch:
|
|
305
|
+
lines.append(f"Differs from your request: {mismatch}")
|
|
306
|
+
lines.extend(service_policy_lines(session.live_service))
|
|
134
307
|
return lines
|
|
135
308
|
|
|
136
309
|
|