booking-agent-cli 0.1.0__py3-none-any.whl

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.
Files changed (74) hide show
  1. booking_agent/__init__.py +6 -0
  2. booking_agent/authorization.py +450 -0
  3. booking_agent/backend/__init__.py +217 -0
  4. booking_agent/backend/live.py +494 -0
  5. booking_agent/calendar/__init__.py +55 -0
  6. booking_agent/calendar/filter.py +41 -0
  7. booking_agent/calendar/google.py +582 -0
  8. booking_agent/calendar/oauth_listener.py +287 -0
  9. booking_agent/calendar/scope.py +97 -0
  10. booking_agent/circuit_breaker.py +116 -0
  11. booking_agent/config.py +51 -0
  12. booking_agent/defaults.env +50 -0
  13. booking_agent/discovery.py +188 -0
  14. booking_agent/headless.py +463 -0
  15. booking_agent/http_deadlines.py +43 -0
  16. booking_agent/http_retry.py +178 -0
  17. booking_agent/http_trace.py +428 -0
  18. booking_agent/merchant_pacing.py +73 -0
  19. booking_agent/models/__init__.py +14 -0
  20. booking_agent/models/platform.py +200 -0
  21. booking_agent/models/wire.py +868 -0
  22. booking_agent/namespace.py +53 -0
  23. booking_agent/observability.py +143 -0
  24. booking_agent/platform_profile.py +120 -0
  25. booking_agent/pop.py +347 -0
  26. booking_agent/profile_cache.py +130 -0
  27. booking_agent/registry_url.py +35 -0
  28. booking_agent/rest_client.py +1493 -0
  29. booking_agent/secure_file.py +52 -0
  30. booking_agent/settings.py +110 -0
  31. booking_agent/signals.py +130 -0
  32. booking_agent/step_timing.py +168 -0
  33. booking_agent/ucp_capabilities.py +148 -0
  34. booking_agent/ucp_checkout.py +1363 -0
  35. booking_agent/ucp_negotiation.py +184 -0
  36. booking_agent/ucp_profile.py +410 -0
  37. booking_agent/usp_methods.py +110 -0
  38. booking_agent_cli/__init__.py +3 -0
  39. booking_agent_cli/activity_log.py +106 -0
  40. booking_agent_cli/booking_poll.py +141 -0
  41. booking_agent_cli/booking_summary.py +151 -0
  42. booking_agent_cli/calendar_connection.py +196 -0
  43. booking_agent_cli/calendar_event.py +611 -0
  44. booking_agent_cli/calendar_preference.py +176 -0
  45. booking_agent_cli/catalog.py +864 -0
  46. booking_agent_cli/checkout_prereq.py +67 -0
  47. booking_agent_cli/commands/__init__.py +0 -0
  48. booking_agent_cli/commands/config.py +106 -0
  49. booking_agent_cli/commands/flow.py +6988 -0
  50. booking_agent_cli/commands/lowlevel.py +163 -0
  51. booking_agent_cli/commands/runtime.py +46 -0
  52. booking_agent_cli/config_loader.py +108 -0
  53. booking_agent_cli/engine.py +168 -0
  54. booking_agent_cli/flow_timings.py +34 -0
  55. booking_agent_cli/geo_eligibility.py +183 -0
  56. booking_agent_cli/geocode.py +233 -0
  57. booking_agent_cli/identity.py +104 -0
  58. booking_agent_cli/link_cli.py +620 -0
  59. booking_agent_cli/main.py +75 -0
  60. booking_agent_cli/output.py +199 -0
  61. booking_agent_cli/payment_helpers.py +884 -0
  62. booking_agent_cli/payment_token.py +23 -0
  63. booking_agent_cli/reschedule.py +137 -0
  64. booking_agent_cli/runtime.py +55 -0
  65. booking_agent_cli/runtime_prepare.py +129 -0
  66. booking_agent_cli/search_area.py +111 -0
  67. booking_agent_cli/session.py +837 -0
  68. booking_agent_cli/support_bundle_schema.py +557 -0
  69. booking_agent_cli/support_report.py +1210 -0
  70. booking_agent_cli/timezone_context.py +314 -0
  71. booking_agent_cli-0.1.0.dist-info/METADATA +75 -0
  72. booking_agent_cli-0.1.0.dist-info/RECORD +74 -0
  73. booking_agent_cli-0.1.0.dist-info/WHEEL +4 -0
  74. booking_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,557 @@
1
+ """Schema of the consented support note, and the check the CLI runs before storing
2
+ or sending one.
3
+
4
+ ``ingest/apps-script/Schema.gs`` is generated from ``SUPPORT_BUNDLE_SCHEMA`` and the
5
+ ingest checks every note against it with ``Validate.gs``. ``validate_bundle`` is the
6
+ same subset validator in Python, so a note the ingest would reject never leaves the
7
+ machine.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from functools import lru_cache
13
+ from typing import Any, Dict, List
14
+
15
+ SUPPORT_BUNDLE_SCHEMA: Dict[str, Any] = {
16
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
17
+ "$id": "https://booking-agent.dev/schemas/support-bundle-v1.json",
18
+ "title": "Booking Agent consented support report, bundle_version 1",
19
+ "description": "Every value is projected from an allowlist. Unknown keys are rejected at every level.",
20
+ "type": "object",
21
+ "additionalProperties": False,
22
+ "required": ["bundle_version", "created_at", "env", "error", "build_errors"],
23
+ "properties": {
24
+ "bundle_version": {"const": 1},
25
+ "created_at": {"$ref": "#/$defs/ts_required"},
26
+ "correlation_id": {"$ref": "#/$defs/id"},
27
+ "env": {
28
+ "type": "object",
29
+ "additionalProperties": False,
30
+ "required": ["cli_version"],
31
+ "properties": {
32
+ "skill_version": {"$ref": "#/$defs/id"},
33
+ "cli_version": {"$ref": "#/$defs/id"},
34
+ "cli_build": {"type": ["string", "null"], "pattern": "^[0-9a-f]{7,64}$"},
35
+ "core_version": {"$ref": "#/$defs/id"},
36
+ "python": {"$ref": "#/$defs/id"},
37
+ "os": {"$ref": "#/$defs/label"},
38
+ "machine": {"$ref": "#/$defs/id"},
39
+ },
40
+ },
41
+ "error": {
42
+ "type": "object",
43
+ "additionalProperties": False,
44
+ "required": ["code"],
45
+ "properties": {
46
+ "code": {"$ref": "#/$defs/id"},
47
+ "phase": {"$ref": "#/$defs/id"},
48
+ "occurrences": {"type": "integer", "minimum": 1},
49
+ "offer_reason": {
50
+ "enum": ["unknown_code", "internal_error", "repeated", "captured_no_next", None],
51
+ },
52
+ "next_command_before_offer": {"$ref": "#/$defs/command"},
53
+ },
54
+ },
55
+ "exception": {
56
+ "anyOf": [
57
+ {"type": "null"},
58
+ {
59
+ "type": "object",
60
+ "additionalProperties": False,
61
+ "properties": {
62
+ "type": {"$ref": "#/$defs/qualname"},
63
+ "chain": {"type": "array", "maxItems": 5, "items": {"$ref": "#/$defs/qualname"}},
64
+ "frames": {
65
+ "type": "array",
66
+ "maxItems": 40,
67
+ "items": {
68
+ "type": "object",
69
+ "additionalProperties": False,
70
+ "properties": {
71
+ "file": {
72
+ "type": "string",
73
+ "maxLength": 300,
74
+ "pattern": "^[^\\x00-\\x1f\\x7f]*$",
75
+ },
76
+ "line": {"type": ["integer", "null"]},
77
+ "function": {"$ref": "#/$defs/qualname"},
78
+ "code": {"type": ["string", "null"], "maxLength": 200},
79
+ },
80
+ },
81
+ },
82
+ },
83
+ },
84
+ ],
85
+ },
86
+ "http": {
87
+ "anyOf": [
88
+ {"type": "null"},
89
+ {
90
+ "type": "object",
91
+ "additionalProperties": False,
92
+ "properties": {
93
+ "method": {"type": "string", "pattern": "^[A-Z]{1,16}$"},
94
+ "host": {
95
+ "type": "string",
96
+ "maxLength": 253,
97
+ "pattern": "^[A-Za-z0-9.:\\[\\]\\-]*$",
98
+ },
99
+ "path": {
100
+ "type": "string",
101
+ "maxLength": 512,
102
+ "pattern": "^[^\\x00-\\x1f\\x7f?#]*$",
103
+ },
104
+ "status": {"type": "integer", "minimum": 100, "maximum": 599},
105
+ "content_type": {
106
+ "type": ["string", "null"],
107
+ "maxLength": 128,
108
+ "pattern": "^[a-z0-9!#$&^_.+\\-]+/[a-z0-9!#$&^_.+\\-]+$",
109
+ },
110
+ "problem_type": {"type": ["string", "null"], "maxLength": 256},
111
+ "ucp_message_codes": {"$ref": "#/$defs/messages"},
112
+ "dropped_codes": {"type": "integer", "minimum": 0},
113
+ "elapsed_ms": {"type": ["number", "null"]},
114
+ },
115
+ },
116
+ ],
117
+ },
118
+ "session": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/session"}]},
119
+ "activity": {
120
+ "anyOf": [
121
+ {"type": "null"},
122
+ {
123
+ "type": "array",
124
+ "maxItems": 50,
125
+ "items": {
126
+ "type": "object",
127
+ "additionalProperties": False,
128
+ "properties": {
129
+ "at": {"$ref": "#/$defs/ts"},
130
+ "kind": {"enum": ["command", "error", None]},
131
+ "command": {"$ref": "#/$defs/command"},
132
+ "code": {"$ref": "#/$defs/id"},
133
+ },
134
+ },
135
+ },
136
+ ],
137
+ },
138
+ "build_errors": {
139
+ "type": "array",
140
+ "maxItems": 10,
141
+ "items": {
142
+ "type": "object",
143
+ "additionalProperties": False,
144
+ "required": ["section", "type"],
145
+ "properties": {
146
+ "section": {"type": "string", "pattern": "^[a-z_]{1,32}$"},
147
+ "type": {"type": "string", "maxLength": 64, "pattern": "^[A-Za-z0-9_]*$"},
148
+ },
149
+ },
150
+ },
151
+ },
152
+ "$defs": {
153
+ "id": {"type": ["string", "null"], "pattern": "^[A-Za-z0-9_.:\\-]{1,160}$"},
154
+ "token": {"type": ["string", "null"], "pattern": "^[a-z0-9_.]{1,64}$"},
155
+ "bool": {"type": ["boolean", "null"]},
156
+ "num": {"type": ["number", "null"]},
157
+ "ts_required": {"type": "string", "maxLength": 48, "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}"},
158
+ "ts": {"type": ["string", "null"], "maxLength": 48, "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}"},
159
+ "tz": {
160
+ "type": ["string", "null"],
161
+ "maxLength": 128,
162
+ "pattern": "^[A-Za-z][A-Za-z0-9_+/\\-]*$",
163
+ },
164
+ "label": {"type": ["string", "null"], "maxLength": 120, "pattern": "^[^\\x00-\\x1f\\x7f]*$"},
165
+ "url": {"type": ["string", "null"], "maxLength": 300, "pattern": "^https?://[^\\s?#]+$"},
166
+ "qualname": {"type": ["string", "null"], "pattern": "^[A-Za-z0-9_.<>]{1,200}$"},
167
+ "command": {"type": ["string", "null"], "maxLength": 4000, "pattern": "^[a-z0-9 &\\-]*$"},
168
+ "id_list": {"type": ["array", "null"], "items": {"$ref": "#/$defs/id"}},
169
+ "messages": {
170
+ "type": ["array", "null"],
171
+ "maxItems": 20,
172
+ "items": {
173
+ "type": "object",
174
+ "additionalProperties": False,
175
+ "required": ["code"],
176
+ "properties": {
177
+ "code": {"type": "string", "pattern": "^[a-z0-9_.]{1,64}$"},
178
+ "severity": {"$ref": "#/$defs/token"},
179
+ },
180
+ },
181
+ },
182
+ "amount": {
183
+ "anyOf": [
184
+ {"type": ["number", "null"]},
185
+ {
186
+ "type": "object",
187
+ "additionalProperties": False,
188
+ "properties": {
189
+ "amount": {"$ref": "#/$defs/num"},
190
+ "amount_minor": {"$ref": "#/$defs/num"},
191
+ "minor_units": {"$ref": "#/$defs/num"},
192
+ "value": {"$ref": "#/$defs/num"},
193
+ "decimal_places": {"$ref": "#/$defs/num"},
194
+ "currency": {"$ref": "#/$defs/id"},
195
+ "model": {"$ref": "#/$defs/id"},
196
+ "payment_timing": {"$ref": "#/$defs/id"},
197
+ },
198
+ },
199
+ ],
200
+ },
201
+ "duration_leaf": {"anyOf": [{"$ref": "#/$defs/id"}, {"type": "number"}]},
202
+ "duration": {
203
+ "anyOf": [
204
+ {"$ref": "#/$defs/duration_leaf"},
205
+ {
206
+ "type": "object",
207
+ "additionalProperties": False,
208
+ "properties": {
209
+ "fixed": {"$ref": "#/$defs/duration_leaf"},
210
+ "min": {"$ref": "#/$defs/duration_leaf"},
211
+ "max": {"$ref": "#/$defs/duration_leaf"},
212
+ },
213
+ },
214
+ ],
215
+ },
216
+ "terms": {
217
+ "anyOf": [
218
+ {"type": "null"},
219
+ {"type": "array", "maxItems": 20, "items": {"$ref": "#/$defs/terms"}},
220
+ {
221
+ "type": "object",
222
+ "additionalProperties": False,
223
+ "properties": {
224
+ "id": {"$ref": "#/$defs/id"},
225
+ "term_id": {"$ref": "#/$defs/id"},
226
+ "kind": {"$ref": "#/$defs/id"},
227
+ "type": {"$ref": "#/$defs/id"},
228
+ "code": {"$ref": "#/$defs/id"},
229
+ "timing": {"$ref": "#/$defs/id"},
230
+ "model": {"$ref": "#/$defs/id"},
231
+ "currency": {"$ref": "#/$defs/id"},
232
+ "amount": {"$ref": "#/$defs/amount"},
233
+ "price": {"$ref": "#/$defs/amount"},
234
+ "due_now": {"$ref": "#/$defs/amount"},
235
+ "amount_minor": {"$ref": "#/$defs/num"},
236
+ "minor_units": {"$ref": "#/$defs/num"},
237
+ "due_now_minor": {"$ref": "#/$defs/num"},
238
+ "percent": {"$ref": "#/$defs/num"},
239
+ "percentage": {"$ref": "#/$defs/num"},
240
+ "due": {"$ref": "#/$defs/ts"},
241
+ "due_at": {"$ref": "#/$defs/ts"},
242
+ "due_date": {"$ref": "#/$defs/ts"},
243
+ "schedule": {"$ref": "#/$defs/terms"},
244
+ "schedules": {"$ref": "#/$defs/terms"},
245
+ "installments": {"$ref": "#/$defs/terms"},
246
+ "payments": {"$ref": "#/$defs/terms"},
247
+ "items": {"$ref": "#/$defs/terms"},
248
+ },
249
+ },
250
+ ],
251
+ },
252
+ "slot": {
253
+ "type": "object",
254
+ "additionalProperties": False,
255
+ "properties": {
256
+ "slot_id": {"$ref": "#/$defs/id"},
257
+ "start": {"$ref": "#/$defs/ts"},
258
+ "end": {"$ref": "#/$defs/ts"},
259
+ "duration": {"$ref": "#/$defs/duration"},
260
+ "price_amount": {"$ref": "#/$defs/num"},
261
+ "alternate_slot_ids": {"$ref": "#/$defs/id_list"},
262
+ },
263
+ },
264
+ "slots": {"type": ["array", "null"], "maxItems": 50, "items": {"$ref": "#/$defs/slot"}},
265
+ "timings": {
266
+ "type": ["object", "null"],
267
+ "additionalProperties": False,
268
+ "patternProperties": {
269
+ "^[a-z0-9_]{1,64}$": {
270
+ "anyOf": [
271
+ {"type": "number"},
272
+ {
273
+ "type": "object",
274
+ "additionalProperties": False,
275
+ "patternProperties": {"^[a-z0-9_]{1,64}$": {"type": "number"}},
276
+ },
277
+ ],
278
+ },
279
+ },
280
+ },
281
+ "session": {
282
+ "type": "object",
283
+ "additionalProperties": False,
284
+ "properties": {
285
+ "id": {"$ref": "#/$defs/id"},
286
+ "created_at": {"$ref": "#/$defs/ts"},
287
+ "schema_version": {"$ref": "#/$defs/num"},
288
+ "flow_phase": {"$ref": "#/$defs/id"},
289
+ "business": {
290
+ "type": ["object", "null"],
291
+ "additionalProperties": False,
292
+ "properties": {
293
+ "profile_url": {"$ref": "#/$defs/url"},
294
+ "rest_endpoint": {"$ref": "#/$defs/url"},
295
+ "business_name": {"$ref": "#/$defs/label"},
296
+ "timezone": {"$ref": "#/$defs/tz"},
297
+ "ucp_checkout": {"type": "boolean"},
298
+ },
299
+ },
300
+ "service_timezone": {"$ref": "#/$defs/tz"},
301
+ "buyer_timezone": {"$ref": "#/$defs/tz"},
302
+ "buyer_timezone_source": {"$ref": "#/$defs/id"},
303
+ "buyer_timezone_error": {"$ref": "#/$defs/id"},
304
+ "timezone_difference_shown": {"$ref": "#/$defs/bool"},
305
+ "slots_presented_dual_zone": {"$ref": "#/$defs/bool"},
306
+ "calendar_gate_completed": {"$ref": "#/$defs/bool"},
307
+ "calendar_state": {"$ref": "#/$defs/id"},
308
+ "calendar_mode": {"$ref": "#/$defs/id"},
309
+ "calendar_route": {"$ref": "#/$defs/id"},
310
+ "calendar_token_env": {"$ref": "#/$defs/id"},
311
+ "link_auth_gate_completed": {"$ref": "#/$defs/bool"},
312
+ "availability_window": {
313
+ "type": ["object", "null"],
314
+ "additionalProperties": False,
315
+ "properties": {"from": {"$ref": "#/$defs/ts"}, "to": {"$ref": "#/$defs/ts"}},
316
+ },
317
+ "slots_presented": {"$ref": "#/$defs/slots"},
318
+ "slot_id": {"$ref": "#/$defs/id"},
319
+ "slot_user_confirmed": {"$ref": "#/$defs/bool"},
320
+ "service_id": {"$ref": "#/$defs/id"},
321
+ "timezone_conflict_confirmed": {"$ref": "#/$defs/bool"},
322
+ "live_service": {
323
+ "type": ["object", "null"],
324
+ "additionalProperties": False,
325
+ "properties": {
326
+ "id": {"$ref": "#/$defs/id"},
327
+ "service_name": {"$ref": "#/$defs/label"},
328
+ "duration": {"$ref": "#/$defs/duration"},
329
+ "price": {"$ref": "#/$defs/amount"},
330
+ "payment_timing": {"$ref": "#/$defs/id"},
331
+ "channel_types": {"$ref": "#/$defs/id_list"},
332
+ },
333
+ },
334
+ "payment_timing": {"$ref": "#/$defs/id"},
335
+ "payment_route": {"$ref": "#/$defs/id"},
336
+ "selected_term_id": {"$ref": "#/$defs/id"},
337
+ "payment_schedules": {"$ref": "#/$defs/terms"},
338
+ "due_now_amount_minor": {"$ref": "#/$defs/num"},
339
+ "payment_disclosures": {"$ref": "#/$defs/terms"},
340
+ "disclosures_user_confirmed": {"$ref": "#/$defs/bool"},
341
+ "total_confirmed_amount_minor": {"$ref": "#/$defs/num"},
342
+ "accepted_term": {"$ref": "#/$defs/terms"},
343
+ "checkout_mode": {"$ref": "#/$defs/id"},
344
+ "supported_checkout_modes": {"$ref": "#/$defs/id_list"},
345
+ "checkout_prereq_completed": {"$ref": "#/$defs/bool"},
346
+ "ucp_checkout_rest_base": {"$ref": "#/$defs/url"},
347
+ "ucp_checkout_session_id": {"$ref": "#/$defs/id"},
348
+ "ucp_checkout_amount_minor": {"$ref": "#/$defs/num"},
349
+ "ucp_merchant_quote": {
350
+ "type": ["object", "null"],
351
+ "additionalProperties": False,
352
+ "patternProperties": {
353
+ "^[a-z0-9_]{1,64}$": {"anyOf": [{"type": ["number", "boolean"]}, {"$ref": "#/$defs/id"}]},
354
+ },
355
+ },
356
+ "ucp_checkout_messages": {"$ref": "#/$defs/messages"},
357
+ "ucp_spt_network_id": {"$ref": "#/$defs/id"},
358
+ "ucp_protocol_version": {"$ref": "#/$defs/id"},
359
+ "ucp_negotiated_capabilities": {
360
+ "type": ["object", "null"],
361
+ "additionalProperties": False,
362
+ "patternProperties": {
363
+ "^[A-Za-z0-9_.:\\-]{1,160}$": {"type": "array", "maxItems": 10, "items": {"$ref": "#/$defs/id"}},
364
+ },
365
+ },
366
+ "ucp_capabilities_authoritative": {"$ref": "#/$defs/bool"},
367
+ "ucp_checkout_cancel_attempted": {"$ref": "#/$defs/bool"},
368
+ "ucp_checkout_cancel_succeeded": {"$ref": "#/$defs/bool"},
369
+ "buyer_source": {"$ref": "#/$defs/label"},
370
+ "booking": {
371
+ "type": ["object", "null"],
372
+ "additionalProperties": False,
373
+ "properties": {
374
+ "id": {"$ref": "#/$defs/id"},
375
+ "checkout_path": {"$ref": "#/$defs/id"},
376
+ "action_expires_at": {"$ref": "#/$defs/ts"},
377
+ "endpoint": {"$ref": "#/$defs/url"},
378
+ "scoped_auth_present": {"type": "boolean"},
379
+ "scoped_auth_expires_at": {"$ref": "#/$defs/ts"},
380
+ "scoped_auth_scope": {"$ref": "#/$defs/id_list"},
381
+ "revision": {"$ref": "#/$defs/id"},
382
+ "status": {"$ref": "#/$defs/id"},
383
+ },
384
+ },
385
+ "payment": {
386
+ "type": ["object", "null"],
387
+ "additionalProperties": False,
388
+ "properties": {
389
+ "status": {"$ref": "#/$defs/id"},
390
+ "spend_request_id": {"$ref": "#/$defs/id"},
391
+ "order_id": {"$ref": "#/$defs/id"},
392
+ "spend_approval_surfaced": {"$ref": "#/$defs/bool"},
393
+ "spend_approval_user_surfaced": {"$ref": "#/$defs/bool"},
394
+ "approved_at": {"$ref": "#/$defs/ts"},
395
+ "spend_action_surfaced": {"$ref": "#/$defs/id"},
396
+ "declined_spend_request_id": {"$ref": "#/$defs/id"},
397
+ "declined_code": {"$ref": "#/$defs/token"},
398
+ "declined_checkout_id": {"$ref": "#/$defs/id"},
399
+ "captured_unconfirmed": {"$ref": "#/$defs/bool"},
400
+ },
401
+ },
402
+ "last_error": {
403
+ "type": ["object", "null"],
404
+ "additionalProperties": False,
405
+ "properties": {"code": {"$ref": "#/$defs/id"}},
406
+ },
407
+ "discover_location_radius_km": {"$ref": "#/$defs/num"},
408
+ "discover_registry_url": {"$ref": "#/$defs/url"},
409
+ "discover_services": {
410
+ "type": ["array", "null"],
411
+ "maxItems": 20,
412
+ "items": {
413
+ "type": "object",
414
+ "additionalProperties": False,
415
+ "properties": {
416
+ "service_id": {"$ref": "#/$defs/id"},
417
+ "service_name": {"$ref": "#/$defs/label"},
418
+ },
419
+ },
420
+ },
421
+ "flow_timings": {"$ref": "#/$defs/timings"},
422
+ "human_gate_timings": {"$ref": "#/$defs/timings"},
423
+ "skill_compliance_validated": {"$ref": "#/$defs/bool"},
424
+ "search_area_confirmed": {"$ref": "#/$defs/bool"},
425
+ "service_address_set": {"$ref": "#/$defs/bool"},
426
+ "calendar_timezone": {"$ref": "#/$defs/tz"},
427
+ "reschedule_original_slot_id": {"$ref": "#/$defs/id"},
428
+ "reschedule_slots": {"$ref": "#/$defs/slots"},
429
+ "reschedule_slot_id": {"$ref": "#/$defs/id"},
430
+ "reschedule_user_confirmed": {"$ref": "#/$defs/bool"},
431
+ },
432
+ },
433
+ },
434
+ }
435
+
436
+ _KEYWORDS = frozenset({
437
+ "$schema", "$id", "title", "description", "$defs", "$ref", "type", "const", "enum",
438
+ "pattern", "maxLength", "minimum", "maximum", "maxItems", "items", "properties",
439
+ "patternProperties", "additionalProperties", "required", "anyOf",
440
+ })
441
+ _DEFS_PREFIX = "#/$defs/"
442
+
443
+
444
+ class _SchemaError(Exception):
445
+ pass
446
+
447
+
448
+ def _type_of(value: Any) -> str:
449
+ if value is None:
450
+ return "null"
451
+ if isinstance(value, bool):
452
+ return "boolean"
453
+ if isinstance(value, int):
454
+ return "integer"
455
+ if isinstance(value, float):
456
+ return "integer" if value.is_integer() else "number"
457
+ if isinstance(value, str):
458
+ return "string"
459
+ if isinstance(value, list):
460
+ return "array"
461
+ if isinstance(value, dict):
462
+ return "object"
463
+ return type(value).__name__
464
+
465
+
466
+ def _type_matches(expected: str, actual: str) -> bool:
467
+ return expected == actual or (expected == "number" and actual == "integer")
468
+
469
+
470
+ def _equal(a: Any, b: Any) -> bool:
471
+ return _type_of(a) == _type_of(b) and a == b
472
+
473
+
474
+ @lru_cache(maxsize=None)
475
+ def _regex(pattern: str) -> "re.Pattern[str]":
476
+ # The ingest's JavaScript `$` does not match before a trailing newline; Python's does.
477
+ if pattern.endswith("$") and not pattern.endswith("\\$"):
478
+ pattern = pattern[:-1] + r"\Z"
479
+ return re.compile(pattern)
480
+
481
+
482
+ def _resolve(root: Dict[str, Any], ref: str) -> Dict[str, Any]:
483
+ if not ref.startswith(_DEFS_PREFIX):
484
+ raise _SchemaError(f"unsupported $ref {ref}")
485
+ target = root.get("$defs", {}).get(ref[len(_DEFS_PREFIX):])
486
+ if target is None:
487
+ raise _SchemaError(f"unknown $ref {ref}")
488
+ return target
489
+
490
+
491
+ def _check(root: Dict[str, Any], schema: Dict[str, Any], value: Any, path: str, errors: List[str]) -> None:
492
+ unknown = set(schema) - _KEYWORDS
493
+ if unknown:
494
+ raise _SchemaError(f"unsupported keyword {sorted(unknown)[0]}")
495
+ if "$ref" in schema:
496
+ _check(root, _resolve(root, schema["$ref"]), value, path, errors)
497
+ if "anyOf" in schema:
498
+ def matches(option: Dict[str, Any]) -> bool:
499
+ sub: List[str] = []
500
+ _check(root, option, value, path, sub)
501
+ return not sub
502
+
503
+ if not any(matches(option) for option in schema["anyOf"]):
504
+ errors.append(f"{path}: matches no anyOf option")
505
+ actual = _type_of(value)
506
+ if "type" in schema:
507
+ types = schema["type"] if isinstance(schema["type"], list) else [schema["type"]]
508
+ if not any(_type_matches(t, actual) for t in types):
509
+ errors.append(f"{path}: expected {'|'.join(types)}, got {actual}")
510
+ return
511
+ if "const" in schema and not _equal(value, schema["const"]):
512
+ errors.append(f"{path}: not {schema['const']}")
513
+ if "enum" in schema and not any(_equal(value, option) for option in schema["enum"]):
514
+ errors.append(f"{path}: not in enum")
515
+ if actual == "string":
516
+ if "maxLength" in schema and len(value) > schema["maxLength"]:
517
+ errors.append(f"{path}: longer than {schema['maxLength']}")
518
+ if "pattern" in schema and not _regex(schema["pattern"]).search(value):
519
+ errors.append(f"{path}: does not match pattern")
520
+ if actual in ("integer", "number"):
521
+ if "minimum" in schema and value < schema["minimum"]:
522
+ errors.append(f"{path}: below minimum")
523
+ if "maximum" in schema and value > schema["maximum"]:
524
+ errors.append(f"{path}: above maximum")
525
+ if actual == "array":
526
+ if "maxItems" in schema and len(value) > schema["maxItems"]:
527
+ errors.append(f"{path}: more than {schema['maxItems']} items")
528
+ if "items" in schema:
529
+ for i, item in enumerate(value):
530
+ _check(root, schema["items"], item, f"{path}[{i}]", errors)
531
+ if actual == "object":
532
+ for key in schema.get("required", []):
533
+ if key not in value:
534
+ errors.append(f"{path}: missing {key}")
535
+ properties = schema.get("properties", {})
536
+ patterns = [(_regex(p), sub) for p, sub in schema.get("patternProperties", {}).items()]
537
+ for key, item in value.items():
538
+ matched = False
539
+ if key in properties:
540
+ matched = True
541
+ _check(root, properties[key], item, f"{path}.{key}", errors)
542
+ for regex, sub in patterns:
543
+ if isinstance(key, str) and regex.search(key):
544
+ matched = True
545
+ _check(root, sub, item, f"{path}.{key}", errors)
546
+ if not matched and schema.get("additionalProperties") is False:
547
+ errors.append(f"{path}: unexpected key")
548
+
549
+
550
+ def validate_bundle(value: Any, schema: Dict[str, Any] = SUPPORT_BUNDLE_SCHEMA) -> List[str]:
551
+ """Every way ``value`` breaks ``schema``; empty means valid. Unsupported schema fails closed."""
552
+ errors: List[str] = []
553
+ try:
554
+ _check(schema, schema, value, "$", errors)
555
+ except (_SchemaError, RecursionError) as e:
556
+ errors.append(f"schema: {e}")
557
+ return errors